395 lines
14 KiB
C++
395 lines
14 KiB
C++
#include "vf/Equipment.h"
|
|
|
|
#include <algorithm>
|
|
|
|
namespace vf {
|
|
|
|
namespace {
|
|
|
|
int slotOffset(EquipSlot slot) { return static_cast<int>(slot); }
|
|
|
|
int handIndex(EquipSlot slot) { return slot == EquipSlot::OffHand ? 1 : 0; }
|
|
|
|
const MetaStack& noStack() {
|
|
static const MetaStack nothing;
|
|
return nothing;
|
|
}
|
|
|
|
const Armor& noArmor() {
|
|
static const Armor nothing;
|
|
return nothing;
|
|
}
|
|
|
|
float swordDamage(ToolTier tier) {
|
|
switch (tier) {
|
|
case ToolTier::Wood: return 4.0f;
|
|
case ToolTier::Stone: return 5.0f;
|
|
case ToolTier::Iron: return 6.0f;
|
|
case ToolTier::Gold: return 4.0f;
|
|
case ToolTier::Diamond: return 7.0f;
|
|
case ToolTier::None: break;
|
|
}
|
|
return unarmedAttackDamage;
|
|
}
|
|
|
|
float axeDamage(ToolTier tier) {
|
|
switch (tier) {
|
|
case ToolTier::Wood: return 7.0f;
|
|
case ToolTier::Stone: return 9.0f;
|
|
case ToolTier::Iron: return 9.0f;
|
|
case ToolTier::Gold: return 7.0f;
|
|
case ToolTier::Diamond: return 9.0f;
|
|
case ToolTier::None: break;
|
|
}
|
|
return unarmedAttackDamage;
|
|
}
|
|
|
|
float pickaxeDamage(ToolTier tier) {
|
|
switch (tier) {
|
|
case ToolTier::Wood: return 2.0f;
|
|
case ToolTier::Stone: return 3.0f;
|
|
case ToolTier::Iron: return 4.0f;
|
|
case ToolTier::Gold: return 2.0f;
|
|
case ToolTier::Diamond: return 5.0f;
|
|
case ToolTier::None: break;
|
|
}
|
|
return unarmedAttackDamage;
|
|
}
|
|
|
|
float axeSpeed(ToolTier tier) {
|
|
switch (tier) {
|
|
case ToolTier::Wood: return 0.8f;
|
|
case ToolTier::Stone: return 0.8f;
|
|
case ToolTier::Iron: return 0.9f;
|
|
case ToolTier::Gold: return 1.0f;
|
|
case ToolTier::Diamond: return 1.0f;
|
|
case ToolTier::None: break;
|
|
}
|
|
return unarmedAttackSpeed;
|
|
}
|
|
|
|
float hoeSpeed(ToolTier tier) {
|
|
switch (tier) {
|
|
case ToolTier::Wood: return 1.0f;
|
|
case ToolTier::Stone: return 2.0f;
|
|
case ToolTier::Iron: return 3.0f;
|
|
case ToolTier::Gold: return 1.0f;
|
|
case ToolTier::Diamond: return 4.0f;
|
|
case ToolTier::None: break;
|
|
}
|
|
return unarmedAttackSpeed;
|
|
}
|
|
|
|
void addModifier(EquipmentStats& stats, AttributeModifier::Attribute attribute,
|
|
AttributeModifier::Operation operation, float amount, EquipSlot slot) {
|
|
if (amount == 0.0f) return;
|
|
AttributeModifier modifier;
|
|
modifier.attribute = attribute;
|
|
modifier.operation = operation;
|
|
modifier.amount = amount;
|
|
modifier.slot = slot;
|
|
stats.modifiers.push_back(modifier);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
const char* equipSlotName(EquipSlot slot) {
|
|
switch (slot) {
|
|
case EquipSlot::Head: return "head";
|
|
case EquipSlot::Chest: return "chest";
|
|
case EquipSlot::Legs: return "legs";
|
|
case EquipSlot::Feet: return "feet";
|
|
case EquipSlot::MainHand: return "mainHand";
|
|
case EquipSlot::OffHand: return "offHand";
|
|
case EquipSlot::Count: break;
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
bool isArmorSlot(EquipSlot slot) { return slotOffset(slot) < static_cast<int>(EquipSlot::MainHand); }
|
|
|
|
bool isHandSlot(EquipSlot slot) {
|
|
return slot == EquipSlot::MainHand || slot == EquipSlot::OffHand;
|
|
}
|
|
|
|
Armor::Slot armorSlotFor(EquipSlot slot) {
|
|
switch (slot) {
|
|
case EquipSlot::Head: return Armor::Slot::Helmet;
|
|
case EquipSlot::Chest: return Armor::Slot::Chestplate;
|
|
case EquipSlot::Legs: return Armor::Slot::Leggings;
|
|
case EquipSlot::Feet: return Armor::Slot::Boots;
|
|
case EquipSlot::MainHand:
|
|
case EquipSlot::OffHand:
|
|
case EquipSlot::Count:
|
|
break;
|
|
}
|
|
return Armor::Slot::Count;
|
|
}
|
|
|
|
EquipSlot equipSlotFor(Armor::Slot slot) {
|
|
switch (slot) {
|
|
case Armor::Slot::Helmet: return EquipSlot::Head;
|
|
case Armor::Slot::Chestplate: return EquipSlot::Chest;
|
|
case Armor::Slot::Leggings: return EquipSlot::Legs;
|
|
case Armor::Slot::Boots: return EquipSlot::Feet;
|
|
case Armor::Slot::Count: break;
|
|
}
|
|
return EquipSlot::Count;
|
|
}
|
|
|
|
const char* attributeName(AttributeModifier::Attribute attribute) {
|
|
switch (attribute) {
|
|
case AttributeModifier::Attribute::AttackDamage: return "attackDamage";
|
|
case AttributeModifier::Attribute::AttackSpeed: return "attackSpeed";
|
|
case AttributeModifier::Attribute::AttackReach: return "attackReach";
|
|
case AttributeModifier::Attribute::MiningSpeed: return "miningSpeed";
|
|
case AttributeModifier::Attribute::MovementSpeed: return "movementSpeed";
|
|
case AttributeModifier::Attribute::ArmorPoints: return "armorPoints";
|
|
case AttributeModifier::Attribute::ArmorToughness: return "armorToughness";
|
|
case AttributeModifier::Attribute::Count: break;
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
float weaponAttackDamage(ItemId item) {
|
|
if (!isTool(item)) return unarmedAttackDamage;
|
|
const ItemInfo& info = itemInfo(item);
|
|
switch (info.toolType) {
|
|
case ToolType::Sword: return swordDamage(info.toolTier);
|
|
case ToolType::Axe: return axeDamage(info.toolTier);
|
|
case ToolType::Pickaxe: return pickaxeDamage(info.toolTier);
|
|
case ToolType::Shovel: return pickaxeDamage(info.toolTier) + 0.5f;
|
|
case ToolType::Hoe: return unarmedAttackDamage;
|
|
case ToolType::None: break;
|
|
}
|
|
return unarmedAttackDamage;
|
|
}
|
|
|
|
float weaponAttackSpeed(ItemId item) {
|
|
if (!isTool(item)) return unarmedAttackSpeed;
|
|
const ItemInfo& info = itemInfo(item);
|
|
switch (info.toolType) {
|
|
case ToolType::Sword: return 1.6f;
|
|
case ToolType::Axe: return axeSpeed(info.toolTier);
|
|
case ToolType::Pickaxe: return 1.2f;
|
|
case ToolType::Shovel: return 1.0f;
|
|
case ToolType::Hoe: return hoeSpeed(info.toolTier);
|
|
case ToolType::None: break;
|
|
}
|
|
return unarmedAttackSpeed;
|
|
}
|
|
|
|
float weaponAttackReach(ItemId item) {
|
|
if (!isTool(item)) return unarmedAttackReach;
|
|
switch (itemInfo(item).toolType) {
|
|
case ToolType::Sword: return unarmedAttackReach + 0.5f;
|
|
case ToolType::Axe:
|
|
case ToolType::Hoe: return unarmedAttackReach + 0.25f;
|
|
case ToolType::Pickaxe:
|
|
case ToolType::Shovel:
|
|
case ToolType::None: break;
|
|
}
|
|
return unarmedAttackReach;
|
|
}
|
|
|
|
float armorWeightPenalty(int armorPoints) {
|
|
if (armorPoints <= 0) return 0.0f;
|
|
float penalty = 0.01f * static_cast<float>(armorPoints);
|
|
return penalty > 0.35f ? 0.35f : penalty;
|
|
}
|
|
|
|
void EquipmentStats::applyTo(CombatStats& stats) const {
|
|
stats.armor = armor;
|
|
stats.baseAttackDamage = attackDamage;
|
|
stats.attackSpeed = attackSpeed;
|
|
}
|
|
|
|
bool Equipment::accepts(EquipSlot slot, const MetaStack& stack) const {
|
|
if (stack.empty()) return false;
|
|
if (!isHandSlot(slot)) return false;
|
|
return true;
|
|
}
|
|
|
|
bool Equipment::equip(EquipSlot slot, const MetaStack& stack) {
|
|
if (!accepts(slot, stack)) return false;
|
|
hands[static_cast<std::size_t>(handIndex(slot))] = stack;
|
|
return true;
|
|
}
|
|
|
|
bool Equipment::equipArmor(const Armor& piece) {
|
|
return equipArmorAt(equipSlotFor(piece.slot), piece);
|
|
}
|
|
|
|
bool Equipment::equipArmorAt(EquipSlot slot, const Armor& piece) {
|
|
if (!isArmorSlot(slot)) return false;
|
|
if (piece.material == Armor::Material::None) return false;
|
|
if (armorSlotFor(slot) != piece.slot) return false;
|
|
worn.at(piece.slot) = piece;
|
|
return true;
|
|
}
|
|
|
|
MetaStack Equipment::unequip(EquipSlot slot) {
|
|
if (!isHandSlot(slot)) return MetaStack{};
|
|
MetaStack removed = hands[static_cast<std::size_t>(handIndex(slot))];
|
|
hands[static_cast<std::size_t>(handIndex(slot))].clear();
|
|
return removed;
|
|
}
|
|
|
|
Armor Equipment::unequipArmor(EquipSlot slot) {
|
|
if (!isArmorSlot(slot)) return Armor{};
|
|
Armor& piece = worn.at(armorSlotFor(slot));
|
|
Armor removed = piece;
|
|
piece = Armor{};
|
|
return removed;
|
|
}
|
|
|
|
const MetaStack& Equipment::hand(EquipSlot slot) const {
|
|
if (!isHandSlot(slot)) return noStack();
|
|
return hands[static_cast<std::size_t>(handIndex(slot))];
|
|
}
|
|
|
|
const Armor& Equipment::armorPiece(EquipSlot slot) const {
|
|
Armor::Slot mapped = armorSlotFor(slot);
|
|
if (mapped == Armor::Slot::Count) return noArmor();
|
|
return worn.at(mapped);
|
|
}
|
|
|
|
EquipmentStats Equipment::stats() const {
|
|
EquipmentStats stats;
|
|
stats.armor = worn;
|
|
stats.armorPoints = worn.totalPoints();
|
|
stats.armorToughness = worn.totalToughness();
|
|
|
|
for (int i = 0; i < static_cast<int>(Armor::Slot::Count); ++i) {
|
|
Armor::Slot armorSlot = static_cast<Armor::Slot>(i);
|
|
const Armor& piece = worn.at(armorSlot);
|
|
if (!piece.worn()) continue;
|
|
EquipSlot slot = equipSlotFor(armorSlot);
|
|
addModifier(stats, AttributeModifier::Attribute::ArmorPoints,
|
|
AttributeModifier::Operation::Add, static_cast<float>(armorPoints(piece)), slot);
|
|
addModifier(stats, AttributeModifier::Attribute::ArmorToughness,
|
|
AttributeModifier::Operation::Add, armorToughness(piece), slot);
|
|
}
|
|
|
|
float weight = armorWeightPenalty(stats.armorPoints);
|
|
stats.movementSpeed = 1.0f - weight;
|
|
addModifier(stats, AttributeModifier::Attribute::MovementSpeed,
|
|
AttributeModifier::Operation::MultiplyTotal, -weight, EquipSlot::Chest);
|
|
|
|
const MetaStack& weapon = hands[0];
|
|
if (!weapon.empty()) {
|
|
stats.weaponEnchants = weapon.meta.enchants;
|
|
stats.attackDamage = weaponAttackDamage(weapon.item);
|
|
stats.attackSpeed = weaponAttackSpeed(weapon.item);
|
|
stats.attackReach = weaponAttackReach(weapon.item);
|
|
stats.miningSpeed = toolTierSpeedMultiplier(itemInfo(weapon.item).toolTier);
|
|
|
|
int efficiency = weapon.meta.enchants.levelOf(EnchantKind::Efficiency);
|
|
if (efficiency > 0) {
|
|
float bonus = static_cast<float>(efficiency * efficiency + 1);
|
|
stats.miningSpeed += bonus;
|
|
addModifier(stats, AttributeModifier::Attribute::MiningSpeed,
|
|
AttributeModifier::Operation::Add, bonus, EquipSlot::MainHand);
|
|
}
|
|
|
|
addModifier(stats, AttributeModifier::Attribute::AttackDamage,
|
|
AttributeModifier::Operation::Add, stats.attackDamage - unarmedAttackDamage,
|
|
EquipSlot::MainHand);
|
|
addModifier(stats, AttributeModifier::Attribute::AttackSpeed,
|
|
AttributeModifier::Operation::Add, stats.attackSpeed - unarmedAttackSpeed,
|
|
EquipSlot::MainHand);
|
|
addModifier(stats, AttributeModifier::Attribute::AttackReach,
|
|
AttributeModifier::Operation::Add, stats.attackReach - unarmedAttackReach,
|
|
EquipSlot::MainHand);
|
|
addModifier(stats, AttributeModifier::Attribute::MiningSpeed,
|
|
AttributeModifier::Operation::Add,
|
|
toolTierSpeedMultiplier(itemInfo(weapon.item).toolTier) - 1.0f,
|
|
EquipSlot::MainHand);
|
|
}
|
|
|
|
return stats;
|
|
}
|
|
|
|
int Equipment::wearFromHit(float incomingDamage, uint32_t& rng) {
|
|
return wearArmor(worn, incomingDamage, rng);
|
|
}
|
|
|
|
WearResult Equipment::useHeld(EquipSlot slot, int amount, uint32_t& rng) {
|
|
if (!isHandSlot(slot)) return WearResult::NotDamageable;
|
|
return damageStack(hands[static_cast<std::size_t>(handIndex(slot))], amount, rng);
|
|
}
|
|
|
|
int Equipment::absorbExperience(int experience) {
|
|
if (experience <= 0) return 0;
|
|
int budget = experience;
|
|
int repaired = 0;
|
|
|
|
for (Armor& piece : worn.pieces) {
|
|
if (budget <= 0) break;
|
|
if (!piece.worn() || !piece.enchants.has(EnchantKind::Mending)) continue;
|
|
int missing = piece.maxDurability - piece.durability;
|
|
if (missing <= 0) continue;
|
|
int restore = std::min(budget * 2, missing);
|
|
piece.durability += restore;
|
|
repaired += restore;
|
|
budget -= (restore + 1) / 2;
|
|
}
|
|
|
|
for (MetaStack& held : hands) {
|
|
if (budget <= 0) break;
|
|
int restore = mendingRepair(held, budget);
|
|
if (restore <= 0) continue;
|
|
repaired += restore;
|
|
budget -= (restore + 1) / 2;
|
|
}
|
|
|
|
return repaired;
|
|
}
|
|
|
|
int Equipment::wornPieces() const {
|
|
int count = 0;
|
|
for (const Armor& piece : worn.pieces) {
|
|
if (piece.worn()) ++count;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
bool Equipment::empty() const {
|
|
return wornPieces() == 0 && hands[0].empty() && hands[1].empty();
|
|
}
|
|
|
|
void loadHandsFromMenu(Equipment& gear, const ContainerMenu& menu, int selectedHotbarSlot) {
|
|
const MenuLayout& layout = menu.layout();
|
|
if (layout.hotbarFirst >= 0 && selectedHotbarSlot >= 0 &&
|
|
selectedHotbarSlot < layout.hotbarCount) {
|
|
gear.mainHandStack() = menu.stackAt(layout.hotbarFirst + selectedHotbarSlot);
|
|
}
|
|
if (layout.offHandSlot >= 0) {
|
|
gear.offHandStack() = menu.stackAt(layout.offHandSlot);
|
|
}
|
|
}
|
|
|
|
void storeHandsIntoMenu(Equipment& gear, ContainerMenu& menu, int selectedHotbarSlot) {
|
|
const MenuLayout& layout = menu.layout();
|
|
if (layout.hotbarFirst >= 0 && selectedHotbarSlot >= 0 &&
|
|
selectedHotbarSlot < layout.hotbarCount) {
|
|
menu.stackAt(layout.hotbarFirst + selectedHotbarSlot) = gear.mainHand();
|
|
}
|
|
if (layout.offHandSlot >= 0) {
|
|
menu.stackAt(layout.offHandSlot) = gear.offHand();
|
|
}
|
|
}
|
|
|
|
float equippedMeleeDamage(const Equipment& gear, const CombatStats& attacker,
|
|
const EffectSet& attackerEffects, bool undeadTarget,
|
|
bool arthropodTarget, bool critical) {
|
|
EquipmentStats stats = gear.stats();
|
|
CombatStats armed = attacker;
|
|
armed.baseAttackDamage = stats.attackDamage;
|
|
armed.attackSpeed = stats.attackSpeed;
|
|
return meleeAttackDamage(armed, stats.weaponEnchants, attackerEffects, undeadTarget,
|
|
arthropodTarget, critical);
|
|
}
|
|
|
|
} // namespace vf
|