diff --git a/CMake/Assets.cmake b/CMake/Assets.cmake index 51739c4960f8..2b8437843630 100644 --- a/CMake/Assets.cmake +++ b/CMake/Assets.cmake @@ -142,6 +142,12 @@ set(devilutionx_assets nlevels/l5data/cornerstone.dun nlevels/l5data/uberroom.dun txtdata/Experience.tsv + txtdata/classes/barbarian/attributes.tsv + txtdata/classes/bard/attributes.tsv + txtdata/classes/monk/attributes.tsv + txtdata/classes/rogue/attributes.tsv + txtdata/classes/sorcerer/attributes.tsv + txtdata/classes/warrior/attributes.tsv ui_art/diablo.pal ui_art/hellfire.pal ui_art/creditsw.clx diff --git a/Packaging/resources/assets/txtdata/classes/barbarian/attributes.tsv b/Packaging/resources/assets/txtdata/classes/barbarian/attributes.tsv new file mode 100644 index 000000000000..95ae7b5186a6 --- /dev/null +++ b/Packaging/resources/assets/txtdata/classes/barbarian/attributes.tsv @@ -0,0 +1,18 @@ +Attribute Value +baseStr 40 +baseMag 0 +baseDex 20 +baseVit 25 +maxStr 255 +maxMag 0 +maxDex 55 +maxVit 150 +blockBonus 30 +adjLife 1152 +adjMana 0 +lvlLife 128 +lvlMana 0 +chrLife 128 +chrMana 64 +itmLife 160 +itmMana 64 diff --git a/Packaging/resources/assets/txtdata/classes/bard/attributes.tsv b/Packaging/resources/assets/txtdata/classes/bard/attributes.tsv new file mode 100644 index 000000000000..bb3d9b02ae72 --- /dev/null +++ b/Packaging/resources/assets/txtdata/classes/bard/attributes.tsv @@ -0,0 +1,18 @@ +Attribute Value +baseStr 20 +baseMag 20 +baseDex 25 +baseVit 20 +maxStr 120 +maxMag 120 +maxDex 120 +maxVit 100 +blockBonus 25 +adjLife 1472 +adjMana 192 +lvlLife 128 +lvlMana 128 +chrLife 64 +chrMana 96 +itmLife 96 +itmMana 112 diff --git a/Packaging/resources/assets/txtdata/classes/monk/attributes.tsv b/Packaging/resources/assets/txtdata/classes/monk/attributes.tsv new file mode 100644 index 000000000000..c20339647e14 --- /dev/null +++ b/Packaging/resources/assets/txtdata/classes/monk/attributes.tsv @@ -0,0 +1,18 @@ +Attribute Value +baseStr 25 +baseMag 15 +baseDex 25 +baseVit 20 +maxStr 150 +maxMag 80 +maxDex 150 +maxVit 80 +blockBonus 25 +adjLife 1472 +adjMana 352 +lvlLife 128 +lvlMana 128 +chrLife 64 +chrMana 64 +itmLife 96 +itmMana 96 diff --git a/Packaging/resources/assets/txtdata/classes/rogue/attributes.tsv b/Packaging/resources/assets/txtdata/classes/rogue/attributes.tsv new file mode 100644 index 000000000000..fe3a72540395 --- /dev/null +++ b/Packaging/resources/assets/txtdata/classes/rogue/attributes.tsv @@ -0,0 +1,18 @@ +Attribute Value +baseStr 20 +baseMag 15 +baseDex 30 +baseVit 20 +maxStr 55 +maxMag 70 +maxDex 250 +maxVit 80 +blockBonus 20 +adjLife 1472 +adjMana 352 +lvlLife 128 +lvlMana 128 +chrLife 64 +chrMana 64 +itmLife 96 +itmMana 96 diff --git a/Packaging/resources/assets/txtdata/classes/sorcerer/attributes.tsv b/Packaging/resources/assets/txtdata/classes/sorcerer/attributes.tsv new file mode 100644 index 000000000000..39f7308d4d0f --- /dev/null +++ b/Packaging/resources/assets/txtdata/classes/sorcerer/attributes.tsv @@ -0,0 +1,18 @@ +Attribute Value +baseStr 15 +baseMag 35 +baseDex 15 +baseVit 20 +maxStr 45 +maxMag 250 +maxDex 85 +maxVit 80 +blockBonus 10 +adjLife 576 +adjMana -128 +lvlLife 64 +lvlMana 128 +chrLife 64 +chrMana 128 +itmLife 64 +itmMana 128 diff --git a/Packaging/resources/assets/txtdata/classes/warrior/attributes.tsv b/Packaging/resources/assets/txtdata/classes/warrior/attributes.tsv new file mode 100644 index 000000000000..cac4626d18a0 --- /dev/null +++ b/Packaging/resources/assets/txtdata/classes/warrior/attributes.tsv @@ -0,0 +1,18 @@ +Attribute Value +baseStr 30 +baseMag 10 +baseDex 20 +baseVit 25 +maxStr 250 +maxMag 50 +maxDex 60 +maxVit 100 +blockBonus 30 +adjLife 1152 +adjMana -64 +lvlLife 128 +lvlMana 64 +chrLife 128 +chrMana 64 +itmLife 128 +itmMana 64 diff --git a/Source/items.cpp b/Source/items.cpp index 0bf384f41de3..8d8a6385c0fe 100644 --- a/Source/items.cpp +++ b/Source/items.cpp @@ -2747,10 +2747,10 @@ void CalcPlrItemVals(Player &player, bool loadgfx) player._pFireResist = std::clamp(fr, 0, MaxResistance); player._pLghtResist = std::clamp(lr, 0, MaxResistance); - vadd = (vadd * PlayersData[static_cast(player._pClass)].itmLife) >> 6; + vadd = (vadd * GetClassAttributes(player._pClass).itmLife) >> 6; ihp += (vadd << 6); // BUGFIX: blood boil can cause negative shifts here (see line 757) - madd = (madd * PlayersData[static_cast(player._pClass)].itmMana) >> 6; + madd = (madd * GetClassAttributes(player._pClass).itmMana) >> 6; imana += (madd << 6); player._pMaxHP = ihp + player._pMaxHPBase; diff --git a/Source/loadsave.cpp b/Source/loadsave.cpp index 9a959b926427..e7889704e88e 100644 --- a/Source/loadsave.cpp +++ b/Source/loadsave.cpp @@ -414,7 +414,7 @@ void LoadPlayer(LoadHelper &file, Player &player) player._pDamageMod = file.NextLE(); player._pBaseToBlk = file.NextLE(); if (player._pBaseToBlk == 0) - player._pBaseToBlk = PlayersData[static_cast(player._pClass)].blockBonus; + player._pBaseToBlk = GetClassAttributes(player._pClass).blockBonus; player._pHPBase = file.NextLE(); player._pMaxHPBase = file.NextLE(); player._pHitPoints = file.NextLE(); diff --git a/Source/pack.cpp b/Source/pack.cpp index 04168b78e133..ddb420fcf653 100644 --- a/Source/pack.cpp +++ b/Source/pack.cpp @@ -453,7 +453,7 @@ void UnPackPlayer(const PlayerPack &packed, Player &player) player._pExperience = SDL_SwapLE32(packed.pExperience); player._pGold = SDL_SwapLE32(packed.pGold); - player._pBaseToBlk = PlayersData[static_cast(player._pClass)].blockBonus; + player._pBaseToBlk = GetClassAttributes(player._pClass).blockBonus; if ((int)(player._pHPBase & 0xFFFFFFC0) < 64) player._pHPBase = 64; @@ -542,7 +542,7 @@ bool UnPackNetPlayer(const PlayerNetPack &packed, Player &player) player._pStatPts = packed.pStatPts; player._pExperience = SDL_SwapLE32(packed.pExperience); - player._pBaseToBlk = PlayersData[static_cast(player._pClass)].blockBonus; + player._pBaseToBlk = GetClassAttributes(player._pClass).blockBonus; player._pMaxManaBase = baseManaMax; player._pManaBase = baseMana; player._pMemSpells = SDL_SwapLE64(packed.pMemSpells); diff --git a/Source/pfile.cpp b/Source/pfile.cpp index 02f6ccd8d0ff..9c5c1c68a615 100644 --- a/Source/pfile.cpp +++ b/Source/pfile.cpp @@ -678,10 +678,11 @@ bool pfile_ui_set_hero_infos(bool (*uiAddHeroInfo)(_uiheroinfo *)) void pfile_ui_set_class_stats(unsigned int playerClass, _uidefaultstats *classStats) { - classStats->strength = PlayersData[playerClass].baseStr; - classStats->magic = PlayersData[playerClass].baseMag; - classStats->dexterity = PlayersData[playerClass].baseDex; - classStats->vitality = PlayersData[playerClass].baseVit; + const ClassAttributes &classAttributes = GetClassAttributes(static_cast(playerClass)); + classStats->strength = classAttributes.baseStr; + classStats->magic = classAttributes.baseMag; + classStats->dexterity = classAttributes.baseDex; + classStats->vitality = classAttributes.baseVit; } uint32_t pfile_ui_get_first_unused_save_num() diff --git a/Source/player.cpp b/Source/player.cpp index 718886411c11..addae961297c 100644 --- a/Source/player.cpp +++ b/Source/player.cpp @@ -1703,16 +1703,16 @@ int Player::GetCurrentAttributeValue(CharacterAttribute attribute) const int Player::GetMaximumAttributeValue(CharacterAttribute attribute) const { - PlayerData plrData = PlayersData[static_cast(_pClass)]; + const ClassAttributes &attr = GetClassAttributes(_pClass); switch (attribute) { case CharacterAttribute::Strength: - return plrData.maxStr; + return attr.maxStr; case CharacterAttribute::Magic: - return plrData.maxMag; + return attr.maxMag; case CharacterAttribute::Dexterity: - return plrData.maxDex; + return attr.maxDex; case CharacterAttribute::Vitality: - return plrData.maxVit; + return attr.maxVit; } app_fatal("Unsupported attribute"); } @@ -2062,14 +2062,14 @@ uint32_t Player::getNextExperienceThreshold() const int32_t Player::calculateBaseLife() const { - const PlayerData &playerData = PlayersData[static_cast(_pClass)]; - return playerData.adjLife + (playerData.lvlLife * getCharacterLevel()) + (playerData.chrLife * _pBaseVit); + const ClassAttributes &attr = GetClassAttributes(_pClass); + return attr.adjLife + (attr.lvlLife * getCharacterLevel()) + (attr.chrLife * _pBaseVit); } int32_t Player::calculateBaseMana() const { - const PlayerData &playerData = PlayersData[static_cast(_pClass)]; - return playerData.adjMana + (playerData.lvlMana * getCharacterLevel()) + (playerData.chrMana * _pBaseMag); + const ClassAttributes &attr = GetClassAttributes(_pClass); + return attr.adjMana + (attr.lvlMana * getCharacterLevel()) + (attr.chrMana * _pBaseMag); } Player *PlayerAtPosition(Point position) @@ -2279,24 +2279,24 @@ void CreatePlayer(Player &player, HeroClass c) player = {}; SetRndSeed(SDL_GetTicks()); - const PlayerData &playerData = PlayersData[static_cast(c)]; + const ClassAttributes &attr = GetClassAttributes(c); player.setCharacterLevel(1); player._pClass = c; - player._pBaseStr = playerData.baseStr; + player._pBaseStr = attr.baseStr; player._pStrength = player._pBaseStr; - player._pBaseMag = playerData.baseMag; + player._pBaseMag = attr.baseMag; player._pMagic = player._pBaseMag; - player._pBaseDex = playerData.baseDex; + player._pBaseDex = attr.baseDex; player._pDexterity = player._pBaseDex; - player._pBaseVit = playerData.baseVit; + player._pBaseVit = attr.baseVit; player._pVitality = player._pBaseVit; - player._pBaseToBlk = playerData.blockBonus; + player._pBaseToBlk = attr.blockBonus; player._pHitPoints = player.calculateBaseLife(); player._pMaxHP = player._pHitPoints; @@ -2314,7 +2314,7 @@ void CreatePlayer(Player &player, HeroClass c) player._pInfraFlag = false; player._pRSplType = SpellType::Skill; - SpellID s = playerData.skill; + SpellID s = PlayersData[static_cast(c)].skill; player._pAblSpells = GetSpellBitmask(s); player._pRSpell = s; @@ -2397,7 +2397,7 @@ void NextPlrLevel(Player &player) } else { player._pStatPts += 5; } - int hp = PlayersData[static_cast(player._pClass)].lvlLife; + int hp = GetClassAttributes(player._pClass).lvlLife; player._pMaxHP += hp; player._pHitPoints = player._pMaxHP; @@ -2408,7 +2408,7 @@ void NextPlrLevel(Player &player) RedrawComponent(PanelDrawComponent::Health); } - int mana = PlayersData[static_cast(player._pClass)].lvlMana; + int mana = GetClassAttributes(player._pClass).lvlMana; player._pMaxMana += mana; player._pMaxManaBase += mana; @@ -3329,7 +3329,7 @@ void ModifyPlrMag(Player &player, int l) player._pBaseMag += l; int ms = l; - ms *= PlayersData[static_cast(player._pClass)].chrMana; + ms *= GetClassAttributes(player._pClass).chrMana; player._pMaxManaBase += ms; player._pMaxMana += ms; @@ -3366,7 +3366,7 @@ void ModifyPlrVit(Player &player, int l) player._pBaseVit += l; int ms = l; - ms *= PlayersData[static_cast(player._pClass)].chrLife; + ms *= GetClassAttributes(player._pClass).chrLife; player._pHPBase += ms; player._pMaxHPBase += ms; @@ -3401,7 +3401,7 @@ void SetPlrMag(Player &player, int v) player._pBaseMag = v; int m = v; - m *= PlayersData[static_cast(player._pClass)].chrMana; + m *= GetClassAttributes(player._pClass).chrMana; player._pMaxManaBase = m; player._pMaxMana = m; @@ -3419,7 +3419,7 @@ void SetPlrVit(Player &player, int v) player._pBaseVit = v; int hp = v; - hp *= PlayersData[static_cast(player._pClass)].chrLife; + hp *= GetClassAttributes(player._pClass).chrLife; player._pHPBase = hp; player._pMaxHPBase = hp; diff --git a/Source/playerdat.cpp b/Source/playerdat.cpp index 99b93b960ee6..f76af6b7f8d6 100644 --- a/Source/playerdat.cpp +++ b/Source/playerdat.cpp @@ -11,6 +11,8 @@ #include #include #include +#include +#include #include #include @@ -22,6 +24,7 @@ #include "textdat.h" #include "utils/language.h" #include "utils/static_vector.hpp" +#include "utils/str_cat.hpp" namespace devilution { @@ -152,11 +155,122 @@ void ReloadExperienceData() } } +void LoadClassAttributes(std::string_view classPath, ClassAttributes &out) +{ + const std::string filename = StrCat("txtdata\\classes\\", classPath, "\\attributes.tsv"); + tl::expected dataFileResult = DataFile::load(filename); + if (!dataFileResult.has_value()) { + DataFile::reportFatalError(dataFileResult.error(), filename); + } + DataFile &dataFile = dataFileResult.value(); + + enum class ClassAttributeColumn { + Attribute, + Value, + LAST = Value + }; + std::array::value> columns; + if (tl::expected result + = dataFile.parseHeader(columns.data(), columns.data() + columns.size(), + [](std::string_view name) -> tl::expected { + if (name == "Attribute") + return ClassAttributeColumn::Attribute; + if (name == "Value") + return ClassAttributeColumn::Value; + return tl::make_unexpected(ColumnDefinition::Error::UnknownColumn); + }); + !result.has_value()) { + DataFile::reportFatalError(result.error(), filename); + } + + struct KeyInfo { + std::variant out; + bool found = false; + }; + std::unordered_map keyToField { + { "baseStr", KeyInfo { &out.baseStr } }, + { "baseMag", KeyInfo { &out.baseMag } }, + { "baseDex", KeyInfo { &out.baseDex } }, + { "baseVit", KeyInfo { &out.baseVit } }, + { "maxStr", KeyInfo { &out.maxStr } }, + { "maxMag", KeyInfo { &out.maxMag } }, + { "maxDex", KeyInfo { &out.maxDex } }, + { "maxVit", KeyInfo { &out.maxVit } }, + { "blockBonus", KeyInfo { &out.blockBonus } }, + { "adjLife", KeyInfo { &out.adjLife } }, + { "adjMana", KeyInfo { &out.adjMana } }, + { "lvlLife", KeyInfo { &out.lvlLife } }, + { "lvlMana", KeyInfo { &out.lvlMana } }, + { "chrLife", KeyInfo { &out.chrLife } }, + { "chrMana", KeyInfo { &out.chrMana } }, + { "itmLife", KeyInfo { &out.itmLife } }, + { "itmMana", KeyInfo { &out.itmMana } }, + }; + for (DataFileRecord record : dataFile) { + FieldIterator fieldIt = record.begin(); + const FieldIterator endField = record.end(); + if (fieldIt == endField) { + DataFile::reportFatalError(DataFile::Error::NotEnoughColumns, filename); + } + + DataFileField keyField = *fieldIt; + const std::string_view key = keyField.value(); + ++fieldIt; + if (fieldIt == endField) { + DataFile::reportFatalError(DataFile::Error::NotEnoughColumns, filename); + } + DataFileField valueField = *fieldIt; + + auto mappingIt = keyToField.find(key); + if (mappingIt == keyToField.end()) { + app_fatal(fmt::format("Unknown field {} in {}", key, filename)); + } + KeyInfo &keyInfo = mappingIt->second; + if (keyInfo.found) { + app_fatal(fmt::format("Duplicate field {} in {}", key, filename)); + } + std::visit([&](auto *outField) { + auto parseIntResult = valueField.parseInt(*outField); + if (parseIntResult != std::errc()) { + DataFile::reportFatalFieldError(parseIntResult, filename, "Value", valueField); + } + }, + keyInfo.out); + keyInfo.found = true; + + ++fieldIt; + } + + for (const auto &[key, keyInfo] : keyToField) { + if (!keyInfo.found) { + app_fatal(fmt::format("Missing field {} in {}", key, filename)); + } + } +} + +std::vector ClassAttributesPerClass; + +void LoadClassesAttributes() +{ + const std::array classPaths { "warrior", "rogue", "sorcerer", "monk", "bard", "barbarian" }; + ClassAttributesPerClass.clear(); + ClassAttributesPerClass.reserve(classPaths.size()); + for (std::string_view path : classPaths) { + LoadClassAttributes(path, ClassAttributesPerClass.emplace_back()); + } +} + } // namespace +const ClassAttributes &GetClassAttributes(HeroClass playerClass) +{ + return ClassAttributesPerClass[static_cast(playerClass)]; +} + void LoadPlayerDataFiles() { ReloadExperienceData(); + LoadClassesAttributes(); } uint32_t GetNextExperienceThresholdForLevel(unsigned level) @@ -183,15 +297,14 @@ const _sfx_id herosounds[enum_size::value][enum_size::val /** Contains the data related to each player class. */ const PlayerData PlayersData[] = { // clang-format off -// HeroClass className, classPath, baseStr, baseMag, baseDex, baseVit, maxStr, maxMag, maxDex, maxVit, blockBonus, adjLife, adjMana, lvlLife, lvlMana, chrLife, chrMana, itmLife, itmMana, skill, - +// HeroClass className, classPath, skill // TRANSLATORS: Player Block start -/* HeroClass::Warrior */ { N_("Warrior"), "warrior", 30, 10, 20, 25, 250, 50, 60, 100, 30, (18 << 6), -(1 << 6), (2 << 6), (1 << 6), (2 << 6), (1 << 6), (2 << 6), (1 << 6), SpellID::ItemRepair }, -/* HeroClass::Rogue */ { N_("Rogue"), "rogue", 20, 15, 30, 20, 55, 70, 250, 80, 20, (23 << 6), static_cast(5.5F * 64), (2 << 6), (2 << 6), (1 << 6), (1 << 6), static_cast(1.5F * 64), static_cast(1.5F * 64), SpellID::TrapDisarm }, -/* HeroClass::Sorcerer */ { N_("Sorcerer"), "sorceror", 15, 35, 15, 20, 45, 250, 85, 80, 10, (9 << 6), -(2 << 6), (1 << 6), (2 << 6), (1 << 6), (2 << 6), (1 << 6), (2 << 6), SpellID::StaffRecharge }, -/* HeroClass::Monk */ { N_("Monk"), "monk", 25, 15, 25, 20, 150, 80, 150, 80, 25, (23 << 6), static_cast(5.5F * 64), (2 << 6), (2 << 6), (1 << 6), (1 << 6), static_cast(1.5F * 64), static_cast(1.5F * 64), SpellID::Search, }, -/* HeroClass::Bard */ { N_("Bard"), "rogue", 20, 20, 25, 20, 120, 120, 120, 100, 25, (23 << 6), (3 << 6), (2 << 6), (2 << 6), (1 << 6), static_cast(1.5F * 64), static_cast(1.5F * 64), static_cast(1.75F * 64), SpellID::Identify }, -/* HeroClass::Barbarian */ { N_("Barbarian"), "warrior", 40, 0, 20, 25, 255, 0, 55, 150, 30, (18 << 6), (0 << 6), (2 << 6), (0 << 6), (2 << 6), (1 << 6), static_cast(2.5F * 64), (1 << 6), SpellID::Rage }, +/* HeroClass::Warrior */ { N_("Warrior"), "warrior", SpellID::ItemRepair }, +/* HeroClass::Rogue */ { N_("Rogue"), "rogue", SpellID::TrapDisarm }, +/* HeroClass::Sorcerer */ { N_("Sorcerer"), "sorceror", SpellID::StaffRecharge }, +/* HeroClass::Monk */ { N_("Monk"), "monk", SpellID::Search }, +/* HeroClass::Bard */ { N_("Bard"), "rogue", SpellID::Identify }, +/* HeroClass::Barbarian */ { N_("Barbarian"), "warrior", SpellID::Rage }, // clang-format on }; diff --git a/Source/playerdat.hpp b/Source/playerdat.hpp index cdc021201cb5..88ec37652b99 100644 --- a/Source/playerdat.hpp +++ b/Source/playerdat.hpp @@ -17,6 +17,11 @@ struct PlayerData { const char *className; /* Class Directory Path */ const char *classPath; + /* Class Skill */ + SpellID skill; +}; + +struct ClassAttributes { /* Class Starting Strength Stat */ uint8_t baseStr; /* Class Starting Magic Stat */ @@ -51,10 +56,10 @@ struct PlayerData { int16_t itmLife; /* Mana from item bonus Magic */ int16_t itmMana; - /* Class Skill */ - SpellID skill; }; +const ClassAttributes &GetClassAttributes(HeroClass playerClass); + struct PlayerSpriteData { /* Sprite width: Stand */ uint8_t stand;