diff --git a/CMake/Assets.cmake b/CMake/Assets.cmake index 51739c4960f..2b843784363 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/README.md b/Packaging/resources/assets/txtdata/classes/README.md new file mode 100644 index 00000000000..92ab3c2fd99 --- /dev/null +++ b/Packaging/resources/assets/txtdata/classes/README.md @@ -0,0 +1,25 @@ +# Player class data + +There is one folder per class. + +### attributes.tsv + + Attribute | Description +-------------:|-------------------------------------- + `baseStr` | Class Starting Strength Stat, uint8_t + `baseMag` | Class Starting Magic Stat, uint8_t + `baseDex` | Class Starting Dexterity Stat, uint8_t + `baseVit` | Class Starting Vitality Stat, uint8_t + `maxStr` | Class Maximum Strength Stat, uint8_t + `maxMag` | Class Maximum Magic Stat, uint8_t + `maxDex` | Class Maximum Dexterity Stat, uint8_t + `maxVit` | Class Maximum Vitality Stat, uint8_t + `blockBonus` | Class Block Bonus, % + `adjLife` | Class Life Adjustment, decimal + `adjMana` | Class Mana Adjustment, decimal + `lvlLife` | Life gained on level up, decimal + `lvlMana` | Mana gained on level up, decimal + `chrLife` | Life from base Vitality, decimal + `chrMana` | Mana from base Magic, decimal + `itmLife` | Life from item bonus Vitality, decimal + `itmMana` | Mana from item bonus Magic, decimal 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 00000000000..58d2dff0ca4 --- /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 18 +adjMana 0 +lvlLife 2 +lvlMana 0 +chrLife 2 +chrMana 1 +itmLife 2.5 +itmMana 1 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 00000000000..42046db254a --- /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 23 +adjMana 3 +lvlLife 2 +lvlMana 2 +chrLife 1 +chrMana 1.5 +itmLife 1.5 +itmMana 1.75 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 00000000000..bb6e98d41fe --- /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 23 +adjMana 5.5 +lvlLife 2 +lvlMana 2 +chrLife 1 +chrMana 1 +itmLife 1.5 +itmMana 1.5 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 00000000000..15a7e2e1101 --- /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 23 +adjMana 5.5 +lvlLife 2 +lvlMana 2 +chrLife 1 +chrMana 1 +itmLife 1.5 +itmMana 1.5 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 00000000000..188d2d49740 --- /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 9 +adjMana -2 +lvlLife 1 +lvlMana 2 +chrLife 1 +chrMana 2 +itmLife 1 +itmMana 2 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 00000000000..cf3fec85d04 --- /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 18 +adjMana -1 +lvlLife 2 +lvlMana 1 +chrLife 2 +chrMana 1 +itmLife 2 +itmMana 1 diff --git a/Source/data/file.cpp b/Source/data/file.cpp index 98aeeea8876..5568dcb7ebb 100644 --- a/Source/data/file.cpp +++ b/Source/data/file.cpp @@ -115,4 +115,16 @@ tl::expected DataFile::parseHeader(ColumnDefinition *begi } return {}; } + +tl::expected DataFile::skipHeader() +{ + RecordIterator it { data(), data() + size(), false }; + ++it; + if (it == this->end()) { + return tl::unexpected { Error::NoContent }; + } + body_ = it.data(); + return {}; +} + } // namespace devilution diff --git a/Source/data/file.hpp b/Source/data/file.hpp index f8eb4e2321e..f687fa9b40f 100644 --- a/Source/data/file.hpp +++ b/Source/data/file.hpp @@ -107,6 +107,8 @@ class DataFile { return parseHeader(begin, end, [typedMapper](std::string_view label) { return typedMapper(label).transform([](T value) { return static_cast(value); }); }); } + [[nodiscard]] tl::expected skipHeader(); + [[nodiscard]] RecordIterator begin() const { return { body_, data() + size(), body_ != data() }; diff --git a/Source/data/iterators.hpp b/Source/data/iterators.hpp index c81745d892b..83f00ee1684 100644 --- a/Source/data/iterators.hpp +++ b/Source/data/iterators.hpp @@ -25,9 +25,9 @@ class DataFileField { static tl::expected mapError(std::errc ec) { - switch (ec) { - case std::errc(): + if (ec == std::errc()) return {}; + switch (ec) { case std::errc::result_out_of_range: return tl::unexpected { Error::OutOfRange }; case std::errc::invalid_argument: diff --git a/Source/items.cpp b/Source/items.cpp index 0bf384f41de..8d8a6385c0f 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 9a959b92642..e7889704e88 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 04168b78e13..ddb420fcf65 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 02f6ccd8d0f..9c5c1c68a61 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 718886411c1..addae961297 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 0458516efa3..addab81c6f4 100644 --- a/Source/playerdat.cpp +++ b/Source/playerdat.cpp @@ -22,6 +22,7 @@ #include "textdat.h" #include "utils/language.h" #include "utils/static_vector.hpp" +#include "utils/str_cat.hpp" namespace devilution { @@ -152,11 +153,103 @@ 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(); + + if (tl::expected result = dataFile.skipHeader(); + !result.has_value()) { + DataFile::reportFatalError(result.error(), filename); + } + + auto recordIt = dataFile.begin(); + const auto recordEnd = dataFile.end(); + + const auto getValueField = [&](std::string_view expectedKey) { + if (recordIt == recordEnd) { + app_fatal(fmt::format("Missing field {} in {}", expectedKey, filename)); + } + DataFileRecord record = *recordIt; + FieldIterator fieldIt = record.begin(); + const FieldIterator endField = record.end(); + + const std::string_view key = (*fieldIt).value(); + if (key != expectedKey) { + app_fatal(fmt::format("Unexpected field in {}: got {}, expected {}", filename, key, expectedKey)); + } + + ++fieldIt; + if (fieldIt == endField) { + DataFile::reportFatalError(DataFile::Error::NotEnoughColumns, filename); + } + return *fieldIt; + }; + + const auto valueReader = [&](auto &&readFn) { + return [&](std::string_view expectedKey, auto &outValue) { + DataFileField valueField = getValueField(expectedKey); + if (const tl::expected result = readFn(valueField, outValue); + !result.has_value()) { + DataFile::reportFatalFieldError(result.error(), filename, "Value", valueField); + } + ++recordIt; + }; + }; + + const auto readInt = valueReader([](DataFileField &valueField, auto &outValue) { + return valueField.parseInt(outValue); + }); + const auto readDecimal = valueReader([](DataFileField &valueField, auto &outValue) { + return valueField.parseFixed6(outValue); + }); + + readInt("baseStr", out.baseStr); + readInt("baseMag", out.baseMag); + readInt("baseDex", out.baseDex); + readInt("baseVit", out.baseVit); + readInt("maxStr", out.maxStr); + readInt("maxMag", out.maxMag); + readInt("maxDex", out.maxDex); + readInt("maxVit", out.maxVit); + readInt("blockBonus", out.blockBonus); + readDecimal("adjLife", out.adjLife); + readDecimal("adjMana", out.adjMana); + readDecimal("lvlLife", out.lvlLife); + readDecimal("lvlMana", out.lvlMana); + readDecimal("chrLife", out.chrLife); + readDecimal("chrMana", out.chrMana); + readDecimal("itmLife", out.itmLife); + readDecimal("itmMana", out.itmMana); +} + +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 +276,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 cdc021201cb..88ec37652b9 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;