From c335a0cab28c6fbd10ae8496a4f4c1ced888a2fc Mon Sep 17 00:00:00 2001 From: ephphatha Date: Sun, 17 Sep 2023 12:44:42 +1000 Subject: [PATCH] Load PlayerData (specifically attributes/life/mana) from file --- CMake/Assets.cmake | 1 + .../resources/assets/txtdata/CharStats.tsv | 8 + Packaging/resources/assets/txtdata/Readme.md | 34 +++ Source/playerdat.cpp | 281 +++++++++++++++++- Source/playerdat.hpp | 2 +- 5 files changed, 312 insertions(+), 14 deletions(-) create mode 100644 Packaging/resources/assets/txtdata/CharStats.tsv diff --git a/CMake/Assets.cmake b/CMake/Assets.cmake index 51739c4960f8..0a098a567b62 100644 --- a/CMake/Assets.cmake +++ b/CMake/Assets.cmake @@ -141,6 +141,7 @@ set(devilutionx_assets nlevels/cutl6w.clx nlevels/l5data/cornerstone.dun nlevels/l5data/uberroom.dun + txtdata/CharStats.tsv txtdata/Experience.tsv ui_art/diablo.pal ui_art/hellfire.pal diff --git a/Packaging/resources/assets/txtdata/CharStats.tsv b/Packaging/resources/assets/txtdata/CharStats.tsv new file mode 100644 index 000000000000..963a817fde52 --- /dev/null +++ b/Packaging/resources/assets/txtdata/CharStats.tsv @@ -0,0 +1,8 @@ +Class Base Strength Base Magic Base Dexterity Base Vitality Maximum Strength Maximum Magic Maximum Dexterity Maximum Vitality Base Life Base Mana Life Per Level Mana Per Level Life Per Player Stat Mana Per Player Stat Life Per Item Stat Mana Per Item Stat +Warrior 30 10 20 25 250 50 60 100 18 -1 2 1 2 1 2 1 +Rogue 20 15 30 20 55 70 250 80 23 5.5 2 2 1 1 1.5 1.5 +Sorcerer 15 35 15 20 45 250 85 80 9 -2 1 2 1 2 1 2 +Expansion +Monk 25 15 25 20 150 80 150 80 23 5.5 2 2 1 1 1.5 1.5 +Bard 20 20 25 20 120 120 120 100 23 3 2 2 1 1.5 1.5 1.75 +Barbarian 40 0 20 25 255 0 55 150 18 0 2 0 2 1 2.5 1 diff --git a/Packaging/resources/assets/txtdata/Readme.md b/Packaging/resources/assets/txtdata/Readme.md index dd1c8cb66d3a..2e672b6ac60b 100644 --- a/Packaging/resources/assets/txtdata/Readme.md +++ b/Packaging/resources/assets/txtdata/Readme.md @@ -93,6 +93,40 @@ format again, refer to the help files provided alongside the game data (`Data/Global/Excel`) (also available online at [D2:R Modding][d2rmodding-utilities]). +### CharStats.tsv +This file contains the starting attributes, natural attribute caps, and +starting life/mana as well as the gain from items/stats. + +#### Class +The class identifier (US English name for the class, so one of "Warrior", +"Rogue", "Sorcerer", etc...). This will be translated into the active language +at runtime. + +#### Base Strength, Base Magic, Base Dexterity, Base Vitality +The starting attribute values for a new character + +#### Maximum Strength, Maximum Magic, Maximum Dexterity, Maximum Vitality +The highest natural value players can obtain by spending the points gained on +level up. This is a soft cap, item bonuses could push the effective attribute +value higher than this. + +#### Base Life, Base Mana +The starting life/mana of a new character. A negative mana value can be used to +restrict spell availability for certain classes, but negative life values will +make a class unplayable. + +#### Life Per Level, Mana Per Level +How much life/mana a character gains each time they level up. + +#### Life Per Player Stat, Mana Per Player Stat +How much life a character gains for each point of Vitality they have naturally, +or how much mana a character gains for each point of Magic they have naturally. + +#### Life Per Item Stat, Mana Per Item Stat +How much life a character gains for each point of Vitality they get through +item bonuses, or how much mana a character gains for each point of Magic they +get through item bonuses. + ### Experience.tsv Experience contains the experience value thresholds before a character advances to the next level. All numeric values in this file MUST be written in diff --git a/Source/playerdat.cpp b/Source/playerdat.cpp index 9bbc3aaec1e9..5db240be0c42 100644 --- a/Source/playerdat.cpp +++ b/Source/playerdat.cpp @@ -153,21 +153,275 @@ void ReloadExperienceData() } /** Contains the data related to each player class. */ -const PlayerData PlayersData[] = { - // clang-format off -// HeroClass className, baseStr, baseMag, baseDex, baseVit, maxStr, maxMag, maxDex, maxVit, adjLife, adjMana, lvlLife, lvlMana, chrLife, chrMana, itmLife, itmMana, - -// TRANSLATORS: Player Block start -/* HeroClass::Warrior */ { N_("Warrior"), 30, 10, 20, 25, 250, 50, 60, 100, (18 << 6), -(1 << 6), (2 << 6), (1 << 6), (2 << 6), (1 << 6), (2 << 6), (1 << 6), }, -/* HeroClass::Rogue */ { N_("Rogue"), 20, 15, 30, 20, 55, 70, 250, 80, (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), }, -/* HeroClass::Sorcerer */ { N_("Sorcerer"), 15, 35, 15, 20, 45, 250, 85, 80, (9 << 6), -(2 << 6), (1 << 6), (2 << 6), (1 << 6), (2 << 6), (1 << 6), (2 << 6), }, -/* HeroClass::Monk */ { N_("Monk"), 25, 15, 25, 20, 150, 80, 150, 80, (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), }, -/* HeroClass::Bard */ { N_("Bard"), 20, 20, 25, 20, 120, 120, 120, 100, (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), }, -// TRANSLATORS: Player Block end -/* HeroClass::Barbarian */ { N_("Barbarian"), 40, 0, 20, 25, 255, 0, 55, 150, (18 << 6), (0 << 6), (2 << 6), (0 << 6), (2 << 6), (1 << 6), static_cast(2.5F * 64), (1 << 6), }, - // clang-format on +std::array::value> PlayersData; + +enum class PlayerDataColumn { + Class, + BaseStrength, + BaseMagic, + BaseDexterity, + BaseVitality, + MaximumStrength, + MaximumMagic, + MaximumDexterity, + MaximumVitality, + LifeAdjustment, + ManaAdjustment, + LifePerLevel, + ManaPerLevel, + LifePerStat, + ManaPerStat, + LifeItemBonus, + ManaItemBonus, + LAST = ManaItemBonus }; +tl::expected mapPlayerDataColumnFromName(std::string_view name) +{ + if (name == "Class") { + return PlayerDataColumn::Class; + } + if (name == "Base Strength") { + return PlayerDataColumn::BaseStrength; + } + if (name == "Base Magic") { + return PlayerDataColumn::BaseMagic; + } + if (name == "Base Dexterity") { + return PlayerDataColumn::BaseDexterity; + } + if (name == "Base Vitality") { + return PlayerDataColumn::BaseVitality; + } + if (name == "Maximum Strength") { + return PlayerDataColumn::MaximumStrength; + } + if (name == "Maximum Magic") { + return PlayerDataColumn::MaximumMagic; + } + if (name == "Maximum Dexterity") { + return PlayerDataColumn::MaximumDexterity; + } + if (name == "Maximum Vitality") { + return PlayerDataColumn::MaximumVitality; + } + if (name == "Base Life") { + return PlayerDataColumn::LifeAdjustment; + } + if (name == "Base Mana") { + return PlayerDataColumn::ManaAdjustment; + } + if (name == "Life Per Level") { + return PlayerDataColumn::LifePerLevel; + } + if (name == "Mana Per Level") { + return PlayerDataColumn::ManaPerLevel; + } + if (name == "Life Per Player Stat") { + return PlayerDataColumn::LifePerStat; + } + if (name == "Mana Per Player Stat") { + return PlayerDataColumn::ManaPerStat; + } + if (name == "Life Per Item Stat") { + return PlayerDataColumn::LifeItemBonus; + } + if (name == "Mana Per Item Stat") { + return PlayerDataColumn::ManaItemBonus; + } + return tl::unexpected { ColumnDefinition::Error::UnknownColumn }; +} + +void ReloadPlayerData() +{ + constexpr std::string_view filename = "txtdata\\CharStats.tsv"; + auto dataFileResult = DataFile::load(filename); + if (!dataFileResult.has_value()) { + DataFile::reportFatalError(dataFileResult.error(), filename); + } + DataFile &dataFile = dataFileResult.value(); + + constexpr unsigned ExpectedColumnCount = enum_size::value; + + std::array columns; + auto parseHeaderResult = dataFile.parseHeader(columns.data(), columns.data() + columns.size(), mapPlayerDataColumnFromName); + + if (!parseHeaderResult.has_value()) { + DataFile::reportFatalError(parseHeaderResult.error(), filename); + } + + for (DataFileRecord record : dataFile) { + HeroClass clazz = static_cast(-1); + PlayerData characterStats {}; + bool skipRecord = false; + + FieldIterator fieldIt = record.begin(); + FieldIterator endField = record.end(); + for (auto &column : columns) { + fieldIt += column.skipLength; + + if (fieldIt == endField) { + DataFile::reportFatalError(DataFile::Error::NotEnoughColumns, filename); + } + + DataFileField field = *fieldIt; + + switch (static_cast(column)) { + case PlayerDataColumn::Class: { + /* TRANSLATORS: Player Class names */ + if (*field == "Warrior") { + clazz = HeroClass::Warrior; + characterStats.className = N_("Warrior"); + } else if (*field == "Rogue") { + clazz = HeroClass::Rogue; + characterStats.className = N_("Rogue"); + } else if (*field == "Sorcerer") { + clazz = HeroClass::Sorcerer; + characterStats.className = N_("Sorcerer"); + } else if (*field == "Monk") { + clazz = HeroClass::Monk; + characterStats.className = N_("Monk"); + } else if (*field == "Bard") { + clazz = HeroClass::Bard; + characterStats.className = N_("Bard"); + } else if (*field == "Barbarian") { + clazz = HeroClass::Barbarian; + characterStats.className = N_("Barbarian"); + } else if (*field == "Expansion") { + // Special marker line used in Diablo 2 text files to separate base game classes from expansion classes. + skipRecord = true; + } else { + DataFile::reportFatalFieldError(DataFileField::Error::InvalidValue, filename, "Class", field); + } + } break; + + case PlayerDataColumn::BaseStrength: { + auto parseIntResult = field.parseInt(characterStats.baseStr); + if (!parseIntResult.has_value()) { + DataFile::reportFatalFieldError(parseIntResult.error(), filename, "Base Strength", field); + } + } break; + + case PlayerDataColumn::BaseMagic: { + auto parseIntResult = field.parseInt(characterStats.baseMag); + if (!parseIntResult.has_value()) { + DataFile::reportFatalFieldError(parseIntResult.error(), filename, "Base Magic", field); + } + } break; + + case PlayerDataColumn::BaseDexterity: { + auto parseIntResult = field.parseInt(characterStats.baseDex); + if (!parseIntResult.has_value()) { + DataFile::reportFatalFieldError(parseIntResult.error(), filename, "Base Dexterity", field); + } + } break; + + case PlayerDataColumn::BaseVitality: { + auto parseIntResult = field.parseInt(characterStats.baseVit); + if (!parseIntResult.has_value()) { + DataFile::reportFatalFieldError(parseIntResult.error(), filename, "Base Vitality", field); + } + } break; + + case PlayerDataColumn::MaximumStrength: { + auto parseIntResult = field.parseInt(characterStats.maxStr); + if (!parseIntResult.has_value()) { + DataFile::reportFatalFieldError(parseIntResult.error(), filename, "Maximum Strength", field); + } + } break; + + case PlayerDataColumn::MaximumMagic: { + auto parseIntResult = field.parseInt(characterStats.maxMag); + if (!parseIntResult.has_value()) { + DataFile::reportFatalFieldError(parseIntResult.error(), filename, "Maximum Magic", field); + } + } break; + + case PlayerDataColumn::MaximumDexterity: { + auto parseIntResult = field.parseInt(characterStats.maxDex); + if (!parseIntResult.has_value()) { + DataFile::reportFatalFieldError(parseIntResult.error(), filename, "Maximum Dexterity", field); + } + } break; + + case PlayerDataColumn::MaximumVitality: { + auto parseIntResult = field.parseInt(characterStats.maxVit); + if (!parseIntResult.has_value()) { + DataFile::reportFatalFieldError(parseIntResult.error(), filename, "Maximum Vitality", field); + } + } break; + + case PlayerDataColumn::LifeAdjustment: { + auto parseIntResult = field.parseFixed6(characterStats.adjLife); + if (!parseIntResult.has_value()) { + DataFile::reportFatalFieldError(parseIntResult.error(), filename, "Base Life", field); + } + } break; + + case PlayerDataColumn::ManaAdjustment: { + auto parseIntResult = field.parseFixed6(characterStats.adjMana); + if (!parseIntResult.has_value()) { + DataFile::reportFatalFieldError(parseIntResult.error(), filename, "Base Mana", field); + } + } break; + + case PlayerDataColumn::LifePerLevel: { + auto parseIntResult = field.parseFixed6(characterStats.lvlLife); + if (!parseIntResult.has_value()) { + DataFile::reportFatalFieldError(parseIntResult.error(), filename, "Life Per Level", field); + } + } break; + + case PlayerDataColumn::ManaPerLevel: { + auto parseIntResult = field.parseFixed6(characterStats.lvlMana); + if (!parseIntResult.has_value()) { + DataFile::reportFatalFieldError(parseIntResult.error(), filename, "Mana Per Level", field); + } + } break; + + case PlayerDataColumn::LifePerStat: { + auto parseIntResult = field.parseFixed6(characterStats.chrLife); + if (!parseIntResult.has_value()) { + DataFile::reportFatalFieldError(parseIntResult.error(), filename, "Life Per Player Stat", field); + } + } break; + + case PlayerDataColumn::ManaPerStat: { + auto parseIntResult = field.parseFixed6(characterStats.chrMana); + if (!parseIntResult.has_value()) { + DataFile::reportFatalFieldError(parseIntResult.error(), filename, "Mana Per Player Stat", field); + } + } break; + + case PlayerDataColumn::LifeItemBonus: { + auto parseIntResult = field.parseFixed6(characterStats.itmLife); + if (!parseIntResult.has_value()) { + DataFile::reportFatalFieldError(parseIntResult.error(), filename, "Life Per Item Stat", field); + } + } break; + + case PlayerDataColumn::ManaItemBonus: { + auto parseIntResult = field.parseFixed6(characterStats.itmMana); + if (!parseIntResult.has_value()) { + DataFile::reportFatalFieldError(parseIntResult.error(), filename, "Mana Per Item Stat", field); + } + } break; + + default: + break; + } + + if (skipRecord) + break; + + ++fieldIt; + } + + if (!skipRecord) + PlayersData[static_cast(clazz)] = characterStats; + } +} + const PlayerCombatData PlayersCombatData[] = { // clang-format off // HeroClass baseToBlock, baseMeleeToHit, baseRangedToHit, baseMagicToHit, @@ -197,6 +451,7 @@ const std::array::value> Players void LoadPlayerDataFiles() { ReloadExperienceData(); + ReloadPlayerData(); } uint32_t GetNextExperienceThresholdForLevel(unsigned level) diff --git a/Source/playerdat.hpp b/Source/playerdat.hpp index 30b37c4fa27f..8eadabe568ef 100644 --- a/Source/playerdat.hpp +++ b/Source/playerdat.hpp @@ -184,7 +184,7 @@ struct PlayerAnimData { }; /** - * @brief Attempts to load data values from external files, currently only Experience.tsv is supported. + * @brief Attempts to load data values from external files (Experience.tsv, CharStats.tsv) */ void LoadPlayerDataFiles();