From d4d3ef73143bfe058c00e229720d01c065eff039 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 2 Sep 2026 19:05:14 +0200 Subject: [PATCH] feat(vf): add Durability and StackMeta --- core/include/vf/ItemStack.h | 170 ++++++++++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 core/include/vf/ItemStack.h diff --git a/core/include/vf/ItemStack.h b/core/include/vf/ItemStack.h new file mode 100644 index 0000000..f8ae49d --- /dev/null +++ b/core/include/vf/ItemStack.h @@ -0,0 +1,170 @@ +#pragma once + +#include +#include +#include + +#include "Enchant.h" +#include "Item.h" +#include "Nbt.h" + +namespace vf { + +// The stack type this header introduces is deliberately NOT called ItemStack: +// vf::ItemStack is already taken by Inventory.h, where it means "a Block plus a +// count" and backs the legacy Hotbar. That older type cannot carry durability, +// enchantments or a custom name, which is why Enchant, Damage and Loot produce +// values with nowhere to live. MetaStack is the stack that can hold them, and +// both names coexist in one translation unit (see testHeaderCoexistence.cpp). + +// Wear on a damageable item. `max` of zero means the item never wears out, +// which is the normal state for blocks, materials and food. +struct Durability { + int used = 0; + int max = 0; + + bool damageable() const { return max > 0; } + int remaining() const { return max > used ? max - used : 0; } + bool broken() const { return max > 0 && used >= max; } + // 1.0 for an untouched item, 0.0 for one about to break. + float fraction() const; + + bool operator==(const Durability& other) const { + return used == other.used && max == other.max; + } + bool operator!=(const Durability& other) const { return !(*this == other); } +}; + +// Everything about one stack beyond "which item, how many". Two stacks merge +// only when their whole StackMeta compares equal, so a half-worn pickaxe never +// silently absorbs a fresh one and an enchanted sword stays distinct from a +// plain one. +struct StackMeta { + Durability durability; + EnchantSet enchants; + std::string customName; + bool unbreakable = false; + // Anvil prior-work penalty, the same number Enchant.h's anvilCombine takes. + int repairCost = 0; + + bool pristine() const; + bool operator==(const StackMeta& other) const; + bool operator!=(const StackMeta& other) const { return !(*this == other); } +}; + +// Order-insensitive comparison of two enchantment sets: the same enchantments +// listed in a different order describe the same item. +bool sameEnchants(const EnchantSet& a, const EnchantSet& b); + +// Item.h's per-item cap. Kept as a one-line forwarder so the stack layer never +// grows a second opinion about how tall a stack may be. +int stackLimitFor(ItemId item); + +struct MetaStack { + ItemId item = ItemId::None; + int count = 0; + StackMeta meta; + + bool empty() const { return count <= 0 || item == ItemId::None; } + // An item carrying per-instance state cannot share a slot with anything, + // so it caps at one regardless of what Item.h says about its kind. + int maxCount() const; + int room() const; + bool full() const { return !empty() && count >= maxCount(); } + + void clear(); +}; + +MetaStack makeStack(ItemId item, int count); +// A tool at full durability, ready to be worn down by damageStack. Named +// makeTool rather than makeToolStack because Item.h already owns that name for +// the ItemSlot flavour, and overloading on return type alone is not a thing. +MetaStack makeTool(ItemId toolItem); +MetaStack makeNamedStack(ItemId item, int count, const std::string& name); +MetaStack makeEnchantedTool(ItemId toolItem, const EnchantSet& enchants); + +// Same item and byte-for-byte the same metadata. Empty stacks never merge. +bool stacksMergeable(const MetaStack& a, const MetaStack& b); +// Mergeable and the same count, which is what a round trip has to reproduce. +bool stacksIdentical(const MetaStack& a, const MetaStack& b); + +struct StackTransfer { + int moved = 0; + bool sourceEmptied = false; + bool destinationFull = false; + // The two stacks could never have merged: different item or different + // metadata. Distinguishes "nothing fitted" from "nothing was allowed". + bool incompatible = false; +}; + +// Moves up to `amount` items from `from` into `into`. An empty destination +// adopts the source's item and metadata; a non-empty one only accepts a +// mergeable stack. Whatever does not fit stays in `from`. +StackTransfer transferStack(MetaStack& from, MetaStack& into, int amount); +StackTransfer transferAll(MetaStack& from, MetaStack& into); + +// Removes up to `amount` and returns it as its own stack, metadata copied. +MetaStack splitOff(MetaStack& stack, int amount); +// The right-click split: the larger half stays on the cursor, matching vanilla. +MetaStack splitHalf(MetaStack& stack); + +// Merges every mergeable pair in place, packing items towards the front of the +// list. Returns how many slots ended up empty that were not empty before. +int defragmentStacks(std::vector& stacks); + +// Total items of one kind across a list, ignoring metadata. +int totalCountOf(const std::vector& stacks, ItemId item); + +// Sort key for an inventory sort button: real items before empty slots, then by +// item id, undamaged before damaged, fuller stacks first, then by custom name. +bool stackOrderLess(const MetaStack& a, const MetaStack& b); +void sortStacks(std::vector& stacks); + +// Xorshift32 shared by every random decision in this layer, so a seed fully +// determines a run. Matches the generator the combat and loot modules use. +uint32_t stackRandom(uint32_t& state); + +// Vanilla Unbreaking: a tool ignores a wear point with probability +// level/(level+1), armour with the flatter 0.6 + 0.4/(level+1) curve that +// Damage.cpp already applies to worn pieces. +bool unbreakingSkipsWear(int unbreakingLevel, bool armorPiece, uint32_t& rng); + +enum class WearResult : uint8_t { + NotDamageable, + Absorbed, // Unbreaking or the unbreakable flag ate the whole hit + Damaged, + Broke, // hit zero durability and the stack is gone + Survived, // hit zero durability but Mending held it at one point left +}; + +// Spends up to `amount` durability, rolling Unbreaking once per point. Clears +// the stack when it breaks. Mending keeps the item alive with a single point +// remaining instead of destroying it. +WearResult damageStack(MetaStack& stack, int amount, uint32_t& rng); + +// Experience repair: two durability points per experience point, capped at what +// is actually missing. Returns the durability restored. +int mendingRepair(MetaStack& stack, int experience); + +// What a recipe leaves behind when this item is used as an ingredient. A +// damageable ingredient is not eaten by the craft: it comes back one point +// worse, and only disappears once that point kills it. Anything else returns an +// empty stack, meaning "consumed". +MetaStack craftingRemainder(const MetaStack& ingredient); + +// Bridge to Container.h, which stores ItemSlot: item, count and the durability +// still left. Enchantments and custom names have no home in an ItemSlot, so a +// round trip through this pair is lossy by construction. Use the Nbt pair below +// whenever the metadata has to survive. +ItemSlot toItemSlot(const MetaStack& stack); +MetaStack fromItemSlot(const ItemSlot& slot); + +// Lossless persistence. An empty stack serialises to a compound holding only a +// zero id, so a fixed-size inventory keeps its slot positions. +NbtTag stackToNbt(const MetaStack& stack); +bool stackFromNbt(const NbtTag& tag, MetaStack& out); + +NbtTag stackListToNbt(const std::vector& stacks); +bool stackListFromNbt(const NbtTag& tag, std::vector& out); + +} // namespace vf