feat(vf): add Durability and StackMeta

This commit is contained in:
dualfroz committed 2026-09-02 19:05:14 +02:00
1 parent 59af42843d
commit d4d3ef7314
1 file changed
+170
+170
View File
@@ -0,0 +1,170 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#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<MetaStack>& stacks);
// Total items of one kind across a list, ignoring metadata.
int totalCountOf(const std::vector<MetaStack>& 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<MetaStack>& 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<MetaStack>& stacks);
bool stackListFromNbt(const NbtTag& tag, std::vector<MetaStack>& out);
} // namespace vf