From 24a8171dabe2efbb22cb1d95933fc28b57e70360 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 2 Sep 2026 18:45:12 +0200 Subject: [PATCH] feat(vf): add AttributeModifier and EquipmentStats --- core/include/vf/Equipment.h | 167 ++++++++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 core/include/vf/Equipment.h diff --git a/core/include/vf/Equipment.h b/core/include/vf/Equipment.h new file mode 100644 index 0000000..bc35ad1 --- /dev/null +++ b/core/include/vf/Equipment.h @@ -0,0 +1,167 @@ +#pragma once + +#include +#include +#include + +#include "ContainerMenu.h" +#include "Damage.h" +#include "ItemStack.h" + +namespace vf { + +// What a player is wearing and holding. The armour half deliberately reuses +// Damage.h's Armor and ArmorSet instead of inventing a second description of a +// breastplate: the whole point is that EquipmentStats hands the combat code +// exactly the values it already knows how to consume. The hand half is made of +// MetaStacks, the same stacks a ContainerMenu moves around. +// +// ItemId has no wearable entries at all, which is why armour cannot be a +// MetaStack here. equip() therefore refuses every stack aimed at an armour +// slot, and equipArmor() is the way in. + +enum class EquipSlot : uint8_t { Head, Chest, Legs, Feet, MainHand, OffHand, Count }; + +const char* equipSlotName(EquipSlot slot); +bool isArmorSlot(EquipSlot slot); +bool isHandSlot(EquipSlot slot); +Armor::Slot armorSlotFor(EquipSlot slot); +EquipSlot equipSlotFor(Armor::Slot slot); + +// One line of the "hover the item and read what it does" tooltip, and the unit +// the stats sum works in. +struct AttributeModifier { + enum class Attribute : uint8_t { + AttackDamage, + AttackSpeed, + AttackReach, + MiningSpeed, + MovementSpeed, + ArmorPoints, + ArmorToughness, + Count + }; + + enum class Operation : uint8_t { Add, MultiplyTotal }; + + Attribute attribute = Attribute::AttackDamage; + Operation operation = Operation::Add; + float amount = 0.0f; + EquipSlot slot = EquipSlot::MainHand; +}; + +const char* attributeName(AttributeModifier::Attribute attribute); + +// Base values a bare player has before anything is worn or held. Named for the +// unarmed case rather than "base", because CombatStats already has fields with +// those names and the two must stay visibly distinct. +constexpr float unarmedAttackDamage = 1.0f; +constexpr float unarmedAttackSpeed = 4.0f; +constexpr float unarmedAttackReach = 3.0f; + +// The damage and swing speed a tool grants when held, keyed off the item +// tables so a new tool tier is picked up without touching this module. +float weaponAttackDamage(ItemId item); +float weaponAttackSpeed(ItemId item); +// Reach and armour weight are this engine's own rules, not ported constants: a +// long hafted weapon reaches further, and every armour point costs a sliver of +// movement speed. They are called out here so nobody mistakes them for vanilla. +float weaponAttackReach(ItemId item); +float armorWeightPenalty(int armorPoints); + +struct EquipmentStats { + // Ready to be assigned onto CombatStats::armor. + ArmorSet armor; + int armorPoints = 0; + float armorToughness = 0.0f; + + float attackDamage = unarmedAttackDamage; + float attackSpeed = unarmedAttackSpeed; + float attackReach = unarmedAttackReach; + // Tool tier times Efficiency: the one attribute that draws on both the item + // and an enchantment without colliding with Damage.h's combat maths. + float miningSpeed = 1.0f; + float movementSpeed = 1.0f; + + // The held weapon's enchantments, handed to Damage.h's meleeAttackDamage + // untouched. Sharpness and the Protection family are deliberately absent + // from attackDamage and from armorPoints: Damage.h already owns those + // formulas, and summing them here as well would count them twice. + EnchantSet weaponEnchants; + + std::vector modifiers; + + // Writes the derived values onto an existing CombatStats without touching + // health, absorption or the invulnerability window. + void applyTo(CombatStats& stats) const; +}; + +class Equipment { +public: + Equipment() = default; + + // Hand slots only. Returns false for an armour slot or an empty stack, + // which is what "only a matching item goes on" means with this item table. + bool equip(EquipSlot slot, const MetaStack& stack); + bool equipArmor(const Armor& piece); + // Places a piece in the slot its own Armor::Slot names, so a helmet cannot + // be forced onto the legs. + bool equipArmorAt(EquipSlot slot, const Armor& piece); + + MetaStack unequip(EquipSlot slot); + Armor unequipArmor(EquipSlot slot); + + bool accepts(EquipSlot slot, const MetaStack& stack) const; + + // Reading by slot is safe for every EquipSlot: an armour slot answers with + // an empty stack and a hand slot with an empty piece, so callers iterating + // the whole enum need no special cases. Writing goes through the named + // accessors below, where the slot is fixed at compile time. + const MetaStack& hand(EquipSlot slot) const; + const Armor& armorPiece(EquipSlot slot) const; + + const MetaStack& mainHand() const { return hands[0]; } + const MetaStack& offHand() const { return hands[1]; } + MetaStack& mainHandStack() { return hands[0]; } + MetaStack& offHandStack() { return hands[1]; } + + const ArmorSet& armor() const { return worn; } + ArmorSet& armor() { return worn; } + + EquipmentStats stats() const; + + // Wears the armour by one point per four damage, honouring Unbreaking and + // dropping a piece that hits zero. Delegates to Damage.h's wearArmor so the + // wear curve stays in one place. Returns the durability spent. + int wearFromHit(float incomingDamage, uint32_t& rng); + + // Wears the held weapon or tool. Returns what happened to it, including the + // break that empties the slot. + WearResult useHeld(EquipSlot slot, int amount, uint32_t& rng); + + // Experience picked up while wearing Mending gear repairs it, gear first + // and the held tool after. Returns the durability restored. + int absorbExperience(int experience); + + int wornPieces() const; + bool empty() const; + +private: + std::array hands; + ArmorSet worn; +}; + +// Reads the player's hands out of an open menu: the selected hotbar slot is the +// main hand, the menu's off-hand slot is the off hand. This is the seam between +// the click layer and the combat layer, and it copies stacks rather than +// aliasing them so a half-finished drag cannot arm the player. +void loadHandsFromMenu(Equipment& gear, const ContainerMenu& menu, int selectedHotbarSlot); +void storeHandsIntoMenu(Equipment& gear, ContainerMenu& menu, int selectedHotbarSlot); + +// Full melee damage against a target, combining the equipment's own attack +// value with the enchantment maths Damage.h already owns. +float equippedMeleeDamage(const Equipment& gear, const CombatStats& attacker, + const EffectSet& attackerEffects, bool undeadTarget, + bool arthropodTarget, bool critical); + +} // namespace vf