diff --git a/core/src/Crafting.cpp b/core/src/Crafting.cpp new file mode 100644 index 0000000..79be818 --- /dev/null +++ b/core/src/Crafting.cpp @@ -0,0 +1,263 @@ +#include "vf/Crafting.h" + +#include + +namespace vf { + +namespace { + +bool matchesShaped(const CraftGrid& grid, const ShapedRecipe& recipe, int minRow, int minCol, + bool mirror) { + for (int r = 0; r < recipe.height; ++r) { + for (int c = 0; c < recipe.width; ++c) { + int patternCol = mirror ? recipe.width - 1 - c : c; + ItemId want = recipe.pattern[r * recipe.width + patternCol]; + ItemId got = grid[(minRow + r) * craftGridWidth + (minCol + c)]; + if (want != got) return false; + } + } + return true; +} + +ShapedRecipe shaped1x2(ItemId top, ItemId bottom, ItemId result, int count) { + ShapedRecipe recipe; + recipe.width = 1; + recipe.height = 2; + recipe.pattern = {top, bottom}; + recipe.result = {result, count}; + recipe.allowMirror = false; + return recipe; +} + +ShapedRecipe shaped1x3(ItemId top, ItemId mid, ItemId bottom, ItemId result, int count) { + ShapedRecipe recipe; + recipe.width = 1; + recipe.height = 3; + recipe.pattern = {top, mid, bottom}; + recipe.result = {result, count}; + recipe.allowMirror = false; + return recipe; +} + +ShapedRecipe shaped2x2(ItemId r0c0, ItemId r0c1, ItemId r1c0, ItemId r1c1, ItemId result, int count) { + ShapedRecipe recipe; + recipe.width = 2; + recipe.height = 2; + recipe.pattern = {r0c0, r0c1, r1c0, r1c1}; + recipe.result = {result, count}; + recipe.allowMirror = true; + return recipe; +} + +ShapedRecipe shaped2x3(ItemId r0c0, ItemId r0c1, ItemId r1c0, ItemId r1c1, ItemId r2c0, ItemId r2c1, + ItemId result, int count) { + ShapedRecipe recipe; + recipe.width = 2; + recipe.height = 3; + recipe.pattern = {r0c0, r0c1, r1c0, r1c1, r2c0, r2c1}; + recipe.result = {result, count}; + recipe.allowMirror = true; + return recipe; +} + +ShapedRecipe shaped3x3(const std::array& pattern, ItemId result, int count, + bool allowMirror = true) { + ShapedRecipe recipe; + recipe.width = 3; + recipe.height = 3; + recipe.pattern = pattern; + recipe.result = {result, count}; + recipe.allowMirror = allowMirror; + return recipe; +} + +struct ToolMaterial { + ToolTier tier; + ItemId material; +}; + +} // namespace + +CraftingRegistry::CraftingRegistry() { registerDefaults(); } + +void CraftingRegistry::addShaped(const ShapedRecipe& recipe) { shaped.push_back(recipe); } +void CraftingRegistry::addShapeless(const ShapelessRecipe& recipe) { shapeless.push_back(recipe); } +void CraftingRegistry::addSmelting(const SmeltingRecipe& recipe) { smelting.push_back(recipe); } + +void CraftingRegistry::registerDefaults() { + const ItemId x = ItemId::None; + + addShapeless({{ItemId::Wood}, {ItemId::Planks, 4}}); + addShaped(shaped1x2(ItemId::Planks, ItemId::Planks, ItemId::Stick, 4)); + addShaped(shaped2x2(ItemId::Planks, ItemId::Planks, ItemId::Planks, ItemId::Planks, + ItemId::CraftingTable, 1)); + addShaped(shaped3x3({ItemId::Cobblestone, ItemId::Cobblestone, ItemId::Cobblestone, + ItemId::Cobblestone, x, ItemId::Cobblestone, + ItemId::Cobblestone, ItemId::Cobblestone, ItemId::Cobblestone}, + ItemId::Furnace, 1, false)); + addShaped(shaped3x3({ItemId::Planks, ItemId::Planks, ItemId::Planks, + ItemId::Planks, x, ItemId::Planks, + ItemId::Planks, ItemId::Planks, ItemId::Planks}, + ItemId::Chest, 1, false)); + addShaped(shaped1x2(ItemId::CoalLump, ItemId::Stick, ItemId::Torch, 4)); + addShaped(shaped3x3({ItemId::Sand, ItemId::Gunpowder, ItemId::Sand, + ItemId::Gunpowder, ItemId::Sand, ItemId::Gunpowder, + ItemId::Sand, ItemId::Gunpowder, ItemId::Sand}, + ItemId::Tnt, 1, false)); + + const ToolMaterial tiers[] = { + {ToolTier::Wood, ItemId::Planks}, + {ToolTier::Stone, ItemId::Cobblestone}, + {ToolTier::Iron, ItemId::IronIngot}, + {ToolTier::Gold, ItemId::GoldIngot}, + {ToolTier::Diamond, ItemId::DiamondGem}, + }; + + for (const ToolMaterial& row : tiers) { + ItemId m = row.material; + ItemId s = ItemId::Stick; + + ItemId pickaxeItem, axeItem, shovelItem, swordItem, hoeItem; + switch (row.tier) { + case ToolTier::Wood: + pickaxeItem = ItemId::WoodPickaxe; axeItem = ItemId::WoodAxe; + shovelItem = ItemId::WoodShovel; swordItem = ItemId::WoodSword; hoeItem = ItemId::WoodHoe; + break; + case ToolTier::Stone: + pickaxeItem = ItemId::StonePickaxe; axeItem = ItemId::StoneAxe; + shovelItem = ItemId::StoneShovel; swordItem = ItemId::StoneSword; hoeItem = ItemId::StoneHoe; + break; + case ToolTier::Iron: + pickaxeItem = ItemId::IronPickaxe; axeItem = ItemId::IronAxe; + shovelItem = ItemId::IronShovel; swordItem = ItemId::IronSword; hoeItem = ItemId::IronHoe; + break; + case ToolTier::Gold: + pickaxeItem = ItemId::GoldPickaxe; axeItem = ItemId::GoldAxe; + shovelItem = ItemId::GoldShovel; swordItem = ItemId::GoldSword; hoeItem = ItemId::GoldHoe; + break; + case ToolTier::Diamond: + pickaxeItem = ItemId::DiamondPickaxe; axeItem = ItemId::DiamondAxe; + shovelItem = ItemId::DiamondShovel; swordItem = ItemId::DiamondSword; hoeItem = ItemId::DiamondHoe; + break; + default: + pickaxeItem = axeItem = shovelItem = swordItem = hoeItem = ItemId::None; + break; + } + + addShaped(shaped3x3({m, m, m, x, s, x, x, s, x}, pickaxeItem, 1, false)); + addShaped(shaped2x3(m, m, m, s, x, s, axeItem, 1)); + addShaped(shaped1x3(m, s, s, shovelItem, 1)); + addShaped(shaped1x3(m, m, s, swordItem, 1)); + addShaped(shaped2x3(m, m, x, s, x, s, hoeItem, 1)); + } + + addSmelting({ItemId::IronOre, {ItemId::IronIngot, 1}, 10.0f}); + addSmelting({ItemId::GoldOre, {ItemId::GoldIngot, 1}, 10.0f}); + addSmelting({ItemId::CoalOre, {ItemId::CoalLump, 1}, 8.0f}); + addSmelting({ItemId::DiamondOre, {ItemId::DiamondGem, 1}, 12.0f}); + addSmelting({ItemId::RedstoneOre, {ItemId::RedstoneDust, 1}, 8.0f}); + addSmelting({ItemId::EmeraldOre, {ItemId::EmeraldGem, 1}, 12.0f}); + addSmelting({ItemId::LapisOre, {ItemId::LapisLazuli, 1}, 8.0f}); + addSmelting({ItemId::Cobblestone, {ItemId::Stone, 1}, 10.0f}); + addSmelting({ItemId::RawPorkchop, {ItemId::CookedPorkchop, 1}, 10.0f}); + addSmelting({ItemId::RawBeef, {ItemId::CookedBeef, 1}, 10.0f}); + addSmelting({ItemId::RawMutton, {ItemId::CookedMutton, 1}, 10.0f}); +} + +CraftResult CraftingRegistry::craftMatch(const CraftGrid& grid) const { + int minRow = craftGridHeight, maxRow = -1, minCol = craftGridWidth, maxCol = -1; + for (int row = 0; row < craftGridHeight; ++row) { + for (int col = 0; col < craftGridWidth; ++col) { + if (grid[row * craftGridWidth + col] == ItemId::None) continue; + minRow = std::min(minRow, row); + maxRow = std::max(maxRow, row); + minCol = std::min(minCol, col); + maxCol = std::max(maxCol, col); + } + } + if (maxRow < 0) return {}; + + int width = maxCol - minCol + 1; + int height = maxRow - minRow + 1; + + for (const ShapedRecipe& recipe : shaped) { + if (recipe.width != width || recipe.height != height) continue; + if (matchesShaped(grid, recipe, minRow, minCol, false)) return recipe.result; + if (recipe.allowMirror && matchesShaped(grid, recipe, minRow, minCol, true)) return recipe.result; + } + + std::vector present; + for (ItemId id : grid) + if (id != ItemId::None) present.push_back(id); + std::sort(present.begin(), present.end()); + + for (const ShapelessRecipe& recipe : shapeless) { + if (recipe.ingredients.size() != present.size()) continue; + std::vector want = recipe.ingredients; + std::sort(want.begin(), want.end()); + if (want == present) return recipe.result; + } + + return {}; +} + +const SmeltingRecipe* CraftingRegistry::smeltingFor(ItemId input) const { + for (const SmeltingRecipe& recipe : smelting) + if (recipe.input == input) return &recipe; + return nullptr; +} + +float fuelBurnSeconds(ItemId item) { + switch (item) { + case ItemId::Stick: return 5.0f; + case ItemId::Planks: return 15.0f; + case ItemId::Wood: return 15.0f; + case ItemId::CoalLump: return 80.0f; + default: return 0.0f; + } +} + +bool isFuel(ItemId item) { return fuelBurnSeconds(item) > 0.0f; } + +Furnace::Furnace(const CraftingRegistry& reg) : registry(®) {} + +void Furnace::tick(float dt) { + const SmeltingRecipe* recipe = input.empty() ? nullptr : registry->smeltingFor(input.item); + if (recipe == nullptr) smeltProgress = 0.0f; + + if (fuelTimeRemaining <= 0.0f && recipe != nullptr && !fuel.empty() && isFuel(fuel.item)) { + float burn = fuelBurnSeconds(fuel.item); + fuelTimeRemaining += burn; + fuelTimeTotal = burn; + fuel.count -= 1; + if (fuel.count <= 0) fuel = ItemSlot{}; + } + + if (fuelTimeRemaining <= 0.0f) return; + fuelTimeRemaining = std::max(0.0f, fuelTimeRemaining - dt); + + if (recipe == nullptr) return; + + bool canOutput = output.empty() || + (output.item == recipe->result.item && + output.count + recipe->result.count <= maxStackSize(output.item)); + if (!canOutput) return; + + smeltProgress += dt; + if (smeltProgress >= recipe->smeltSeconds) { + smeltProgress -= recipe->smeltSeconds; + if (output.empty()) output = ItemSlot{recipe->result.item, recipe->result.count, 0}; + else output.count += recipe->result.count; + input.count -= 1; + if (input.count <= 0) input = ItemSlot{}; + } +} + +float Furnace::smeltFraction() const { + if (input.empty()) return 0.0f; + const SmeltingRecipe* recipe = registry->smeltingFor(input.item); + if (recipe == nullptr || recipe->smeltSeconds <= 0.0f) return 0.0f; + return smeltProgress / recipe->smeltSeconds; +} + +} // namespace vf