From d87c0dcf8a922e474ac025cd6ac78580446ddcc3 Mon Sep 17 00:00:00 2001 From: Andrettin <6322423+Andrettin@users.noreply.github.com> Date: Sun, 7 Sep 2025 04:36:57 +0200 Subject: [PATCH] Player Class Flags (#8173) --- Source/data/value_reader.hpp | 13 +++++++++++ Source/inv.cpp | 7 ++++-- Source/items.cpp | 20 +++++++++++------ Source/objects.cpp | 3 ++- Source/player.cpp | 7 ++++-- Source/playerdat.cpp | 10 +++++++++ Source/playerdat.hpp | 22 +++++++++++++++++++ .../txtdata/classes/barbarian/attributes.tsv | 1 + assets/txtdata/classes/bard/attributes.tsv | 1 + assets/txtdata/classes/monk/attributes.tsv | 1 + assets/txtdata/classes/rogue/attributes.tsv | 1 + .../txtdata/classes/sorcerer/attributes.tsv | 1 + assets/txtdata/classes/warrior/attributes.tsv | 1 + 13 files changed, 76 insertions(+), 12 deletions(-) diff --git a/Source/data/value_reader.hpp b/Source/data/value_reader.hpp index 808912abb..6859db307 100644 --- a/Source/data/value_reader.hpp +++ b/Source/data/value_reader.hpp @@ -46,6 +46,19 @@ public: }); } + template + void readEnumList(std::string_view expectedKey, T &outValue, F &&parseFn) + { + readValue(expectedKey, outValue, [&parseFn](DataFileField &valueField, T &outValue) -> tl::expected { + const auto result = valueField.parseEnumList(outValue, std::forward(parseFn)); + if (!result.has_value()) { + return tl::make_unexpected(devilution::DataFileField::Error::InvalidValue); + } + + return {}; + }); + } + template typename std::enable_if_t, void> readInt(std::string_view expectedKey, T &outValue) diff --git a/Source/inv.cpp b/Source/inv.cpp index 168285b25..cbcb8f54b 100644 --- a/Source/inv.cpp +++ b/Source/inv.cpp @@ -207,7 +207,8 @@ bool CanWield(Player &player, const Item &item) // Bard can dual wield swords and maces, so we allow equiping one-handed weapons in her free slot as long as her occupied // slot is another one-handed weapon. - if (player._pClass == HeroClass::Bard) { + const ClassAttributes &classAttributes = GetClassAttributes(player._pClass); + if (HasAnyOf(classAttributes.classFlags, PlayerClassFlag::DualWield)) { const bool occupiedHandIsOneHandedSwordOrMace = player.GetItemLocation(occupiedHand) == ILOC_ONEHAND && IsAnyOf(occupiedHand._itype, ItemType::Sword, ItemType::Mace); @@ -358,8 +359,10 @@ void ChangeEquippedItem(Player &player, uint8_t slot) const inv_body_loc selectedHand = slot == SLOTXY_HAND_LEFT ? INVLOC_HAND_LEFT : INVLOC_HAND_RIGHT; const inv_body_loc otherHand = slot == SLOTXY_HAND_LEFT ? INVLOC_HAND_RIGHT : INVLOC_HAND_LEFT; + const ClassAttributes &classAttributes = GetClassAttributes(player._pClass); + const bool pasteIntoSelectedHand = (player.InvBody[otherHand].isEmpty() || player.InvBody[otherHand]._iClass != player.HoldItem._iClass) - || (player._pClass == HeroClass::Bard && player.InvBody[otherHand]._iClass == ICLASS_WEAPON && player.HoldItem._iClass == ICLASS_WEAPON); + || (HasAnyOf(classAttributes.classFlags, PlayerClassFlag::DualWield) && player.InvBody[otherHand]._iClass == ICLASS_WEAPON && player.HoldItem._iClass == ICLASS_WEAPON); const bool dequipTwoHandedWeapon = (!player.InvBody[otherHand].isEmpty() && player.GetItemLocation(player.InvBody[otherHand]) == ILOC_TWOHAND); diff --git a/Source/items.cpp b/Source/items.cpp index 87219d541..c2f205a1e 100644 --- a/Source/items.cpp +++ b/Source/items.cpp @@ -2553,14 +2553,14 @@ void CalcPlrDamageMod(Player &player) switch (player._pClass) { case HeroClass::Rogue: player._pDamageMod = strDexMod / 200; - return; + break; case HeroClass::Monk: if (player.isHoldingItem(ItemType::Staff) || (leftHandItem.isEmpty() && rightHandItem.isEmpty())) { player._pDamageMod = strDexMod / 150; } else { player._pDamageMod = strDexMod / 300; } - return; + break; case HeroClass::Bard: if (player.isHoldingItem(ItemType::Sword)) { player._pDamageMod = strDexMod / 150; @@ -2569,7 +2569,7 @@ void CalcPlrDamageMod(Player &player) } else { player._pDamageMod = strMod / 100; } - return; + break; case HeroClass::Barbarian: if (player.isHoldingItem(ItemType::Axe) || player.isHoldingItem(ItemType::Mace)) { player._pDamageMod = strMod / 75; @@ -2586,11 +2586,15 @@ void CalcPlrDamageMod(Player &player) } else if (!player.isHoldingItem(ItemType::Staff) && !player.isHoldingItem(ItemType::Bow)) { player._pDamageMod += playerLevel * player._pVitality / 100; } - player._pIAC += playerLevel / 4; - return; + break; default: player._pDamageMod = strMod / 100; - return; + break; + } + + const ClassAttributes &classAttributes = GetClassAttributes(player._pClass); + if (HasAnyOf(classAttributes.classFlags, PlayerClassFlag::IronSkin)) { + player._pIAC += playerLevel / 4; } } @@ -2598,7 +2602,9 @@ void CalcPlrResistances(Player &player, ItemSpecialEffect iflgs, int fire, int l { const uint8_t playerLevel = player.getCharacterLevel(); - if (player._pClass == HeroClass::Barbarian) { + const ClassAttributes &classAttributes = GetClassAttributes(player._pClass); + + if (HasAnyOf(classAttributes.classFlags, PlayerClassFlag::NaturalResistance)) { magic += playerLevel; fire += playerLevel; lightning += playerLevel; diff --git a/Source/objects.cpp b/Source/objects.cpp index c8f7eede3..955196096 100644 --- a/Source/objects.cpp +++ b/Source/objects.cpp @@ -4890,7 +4890,8 @@ StringOrView Object::name() const void GetObjectStr(const Object &object) { InfoString = object.name(); - if (MyPlayer->_pClass == HeroClass::Rogue) { + const ClassAttributes &classAttributes = GetClassAttributes(MyPlayer->_pClass); + if (HasAnyOf(classAttributes.classFlags, PlayerClassFlag::TrapSense)) { if (object._oTrapFlag) { InfoString = fmt::format(fmt::runtime(_(/* TRANSLATORS: {:s} will either be a chest or a door */ "Trapped {:s}")), InfoString.str()); InfoColor = UiFlags::ColorRed; diff --git a/Source/player.cpp b/Source/player.cpp index 69e36dd95..1e439e867 100644 --- a/Source/player.cpp +++ b/Source/player.cpp @@ -563,7 +563,9 @@ bool PlrHitMonst(Player &player, Monster &monster, bool adjacentDamage = false) dam += player._pIBonusDamMod; int dam2 = dam << 6; dam += player._pDamageMod; - if (player._pClass == HeroClass::Warrior || player._pClass == HeroClass::Barbarian) { + + const ClassAttributes &classAttributes = GetClassAttributes(player._pClass); + if (HasAnyOf(classAttributes.classFlags, PlayerClassFlag::CriticalStrike)) { if (GenerateRnd(100) < player.getCharacterLevel()) { dam *= 2; } @@ -730,7 +732,8 @@ bool PlrHitPlr(Player &attacker, Player &target) dam += (dam * attacker._pIBonusDam) / 100; dam += attacker._pIBonusDamMod + attacker._pDamageMod; - if (attacker._pClass == HeroClass::Warrior || attacker._pClass == HeroClass::Barbarian) { + const ClassAttributes &classAttributes = GetClassAttributes(attacker._pClass); + if (HasAnyOf(classAttributes.classFlags, PlayerClassFlag::CriticalStrike)) { if (GenerateRnd(100) < attacker.getCharacterLevel()) { dam *= 2; } diff --git a/Source/playerdat.cpp b/Source/playerdat.cpp index 6195b441d..3a7813d15 100644 --- a/Source/playerdat.cpp +++ b/Source/playerdat.cpp @@ -156,6 +156,15 @@ void ReloadExperienceData() } } +tl::expected ParsePlayerClassFlag(std::string_view value) +{ + const std::optional enumValueOpt = magic_enum::enum_cast(value); + if (enumValueOpt.has_value()) { + return enumValueOpt.value(); + } + return tl::make_unexpected("Unknown enum value"); +} + void LoadClassData(std::string_view classPath, ClassAttributes &attributes, PlayerCombatData &combat) { const std::string filename = StrCat("txtdata\\classes\\", classPath, "\\attributes.tsv"); @@ -165,6 +174,7 @@ void LoadClassData(std::string_view classPath, ClassAttributes &attributes, Play ValueReader reader { dataFile, filename }; + reader.readEnumList("classFlags", attributes.classFlags, ParsePlayerClassFlag); reader.readInt("baseStr", attributes.baseStr); reader.readInt("baseMag", attributes.baseMag); reader.readInt("baseDex", attributes.baseDex); diff --git a/Source/playerdat.hpp b/Source/playerdat.hpp index b7c934171..9af28ccd0 100644 --- a/Source/playerdat.hpp +++ b/Source/playerdat.hpp @@ -27,6 +27,20 @@ enum class HeroClass : uint8_t { LAST = Barbarian, }; +enum class PlayerClassFlag : uint8_t { + // clang-format off + None = 0, + CriticalStrike = 1 << 0, + DualWield = 1 << 1, + IronSkin = 1 << 2, + NaturalResistance = 1 << 3, + TrapSense = 1 << 4, + + Last = TrapSense + // clang-format on +}; +use_enum_as_flags(PlayerClassFlag); + struct PlayerData { /* Class Name */ std::string className; @@ -39,6 +53,8 @@ struct PlayerData { }; struct ClassAttributes { + /* Class Flags */ + PlayerClassFlag classFlags; /* Class Starting Strength Stat */ uint8_t baseStr; /* Class Starting Magic Stat */ @@ -213,3 +229,9 @@ const PlayerSpriteData &GetPlayerSpriteDataForClass(HeroClass clazz); const PlayerAnimData &GetPlayerAnimDataForClass(HeroClass clazz); } // namespace devilution + +template <> +struct magic_enum::customize::enum_range { + static constexpr uint8_t min = static_cast(devilution::PlayerClassFlag::None); + static constexpr uint8_t max = static_cast(devilution::PlayerClassFlag::Last); +}; diff --git a/assets/txtdata/classes/barbarian/attributes.tsv b/assets/txtdata/classes/barbarian/attributes.tsv index 2da44432d..7b1d1c875 100644 --- a/assets/txtdata/classes/barbarian/attributes.tsv +++ b/assets/txtdata/classes/barbarian/attributes.tsv @@ -1,4 +1,5 @@ Attribute Value +classFlags CriticalStrike,IronSkin,NaturalResistance baseStr 40 baseMag 0 baseDex 20 diff --git a/assets/txtdata/classes/bard/attributes.tsv b/assets/txtdata/classes/bard/attributes.tsv index 4548b016a..c1b7d4bf1 100644 --- a/assets/txtdata/classes/bard/attributes.tsv +++ b/assets/txtdata/classes/bard/attributes.tsv @@ -1,4 +1,5 @@ Attribute Value +classFlags DualWield baseStr 20 baseMag 20 baseDex 25 diff --git a/assets/txtdata/classes/monk/attributes.tsv b/assets/txtdata/classes/monk/attributes.tsv index 4d27f9ad8..44ecdadfd 100644 --- a/assets/txtdata/classes/monk/attributes.tsv +++ b/assets/txtdata/classes/monk/attributes.tsv @@ -1,4 +1,5 @@ Attribute Value +classFlags baseStr 25 baseMag 15 baseDex 25 diff --git a/assets/txtdata/classes/rogue/attributes.tsv b/assets/txtdata/classes/rogue/attributes.tsv index 8731a9a3e..76892b6b0 100644 --- a/assets/txtdata/classes/rogue/attributes.tsv +++ b/assets/txtdata/classes/rogue/attributes.tsv @@ -1,4 +1,5 @@ Attribute Value +classFlags TrapSense baseStr 20 baseMag 15 baseDex 30 diff --git a/assets/txtdata/classes/sorcerer/attributes.tsv b/assets/txtdata/classes/sorcerer/attributes.tsv index 4cc0445e1..cf9a74356 100644 --- a/assets/txtdata/classes/sorcerer/attributes.tsv +++ b/assets/txtdata/classes/sorcerer/attributes.tsv @@ -1,4 +1,5 @@ Attribute Value +classFlags baseStr 15 baseMag 35 baseDex 15 diff --git a/assets/txtdata/classes/warrior/attributes.tsv b/assets/txtdata/classes/warrior/attributes.tsv index 447298fb6..725e44ae3 100644 --- a/assets/txtdata/classes/warrior/attributes.tsv +++ b/assets/txtdata/classes/warrior/attributes.tsv @@ -1,4 +1,5 @@ Attribute Value +classFlags CriticalStrike baseStr 30 baseMag 10 baseDex 20