From da76e131e492fd636bb78583e16318d2ff5e2b62 Mon Sep 17 00:00:00 2001 From: ephphatha Date: Sun, 23 Jul 2023 18:04:12 +1000 Subject: [PATCH] Add getter/setter for character levels to ensure _pNextExper stays synced --- Source/control.cpp | 2 +- Source/diablo.cpp | 2 +- Source/discord/discord.cpp | 2 +- Source/items.cpp | 68 +++++++++++------------ Source/levels/trigs.cpp | 12 ++--- Source/loadsave.cpp | 14 ++--- Source/missiles.cpp | 104 ++++++++++++++++++------------------ Source/monster.cpp | 4 +- Source/msg.cpp | 6 +-- Source/multi.cpp | 10 ++-- Source/objects.cpp | 4 +- Source/pack.cpp | 8 +-- Source/panels/charpanel.cpp | 6 +-- Source/pfile.cpp | 2 +- Source/player.cpp | 57 ++++++++++---------- Source/player.h | 26 +++++++-- Source/plrmsg.cpp | 2 +- Source/qol/chatlog.cpp | 2 +- Source/qol/xpbar.cpp | 8 +-- Source/spells.cpp | 4 +- Source/stores.cpp | 4 +- Source/towners.cpp | 4 +- test/player_test.cpp | 6 ++- test/writehero_test.cpp | 2 +- 24 files changed, 190 insertions(+), 169 deletions(-) diff --git a/Source/control.cpp b/Source/control.cpp index 4e355557a..39c7b81d4 100644 --- a/Source/control.cpp +++ b/Source/control.cpp @@ -1205,7 +1205,7 @@ void DrawInfoBox(const Surface &out) InfoColor = UiFlags::ColorWhitegold; auto &target = Players[pcursplr]; InfoString = std::string_view(target._pName); - AddPanelString(fmt::format(fmt::runtime(_("{:s}, Level: {:d}")), _(PlayersData[static_cast(target._pClass)].className), target._pLevel)); + AddPanelString(fmt::format(fmt::runtime(_("{:s}, Level: {:d}")), _(PlayersData[static_cast(target._pClass)].className), target.getCharacterLevel())); AddPanelString(fmt::format(fmt::runtime(_("Hit Points {:d} of {:d}")), target._pHitPoints >> 6, target._pMaxHP >> 6)); } } diff --git a/Source/diablo.cpp b/Source/diablo.cpp index 1f646c2e4..a459efbc7 100644 --- a/Source/diablo.cpp +++ b/Source/diablo.cpp @@ -2493,7 +2493,7 @@ bool TryIconCurs() DoRepair(myPlayer, pcursinvitem); else if (pcursstashitem != StashStruct::EmptyCell) { Item &item = Stash.stashList[pcursstashitem]; - RepairItem(item, myPlayer._pLevel); + RepairItem(item, myPlayer.getCharacterLevel()); } NewCursor(CURSOR_HAND); return true; diff --git a/Source/discord/discord.cpp b/Source/discord/discord.cpp index a0345168f..63d267d6a 100644 --- a/Source/discord/discord.cpp +++ b/Source/discord/discord.cpp @@ -137,7 +137,7 @@ void UpdateGame() return; auto newData = PlayerData { - leveltype, setlvlnum, currlevel, MyPlayer->_pLevel, MyPlayer->_pgfxnum + leveltype, setlvlnum, currlevel, MyPlayer->getCharacterLevel(), MyPlayer->_pgfxnum }; if (newData != tracked_data) { tracked_data = newData; diff --git a/Source/items.cpp b/Source/items.cpp index d73dafce6..0bf384f41 100644 --- a/Source/items.cpp +++ b/Source/items.cpp @@ -2623,6 +2623,8 @@ void CalcPlrItemVals(Player &player, bool loadgfx) } } + const uint8_t playerLevel = player.getCharacterLevel(); + if (mind == 0 && maxd == 0) { mind = 1; maxd = 1; @@ -2636,20 +2638,20 @@ void CalcPlrItemVals(Player &player, bool loadgfx) } if (player._pClass == HeroClass::Monk) { - mind = std::max(mind, player._pLevel / 2); - maxd = std::max(maxd, (int)player._pLevel); + mind = std::max(mind, playerLevel / 2); + maxd = std::max(maxd, playerLevel); } } if (HasAnyOf(player._pSpellFlags, SpellFlag::RageActive)) { - sadd += 2 * player._pLevel; - dadd += player._pLevel + player._pLevel / 2; - vadd += 2 * player._pLevel; + sadd += 2 * playerLevel; + dadd += playerLevel + playerLevel / 2; + vadd += 2 * playerLevel; } if (HasAnyOf(player._pSpellFlags, SpellFlag::RageCooldown)) { - sadd -= 2 * player._pLevel; - dadd -= player._pLevel + player._pLevel / 2; - vadd -= 2 * player._pLevel; + sadd -= 2 * playerLevel; + dadd -= playerLevel + playerLevel / 2; + vadd -= 2 * playerLevel; } player._pIMinDam = mind; @@ -2677,29 +2679,29 @@ void CalcPlrItemVals(Player &player, bool loadgfx) player._pVitality = std::max(0, vadd + player._pBaseVit); if (player._pClass == HeroClass::Rogue) { - player._pDamageMod = player._pLevel * (player._pStrength + player._pDexterity) / 200; + player._pDamageMod = playerLevel * (player._pStrength + player._pDexterity) / 200; } else if (player._pClass == HeroClass::Monk) { - player._pDamageMod = player._pLevel * (player._pStrength + player._pDexterity) / 150; + player._pDamageMod = playerLevel * (player._pStrength + player._pDexterity) / 150; if ((!player.InvBody[INVLOC_HAND_LEFT].isEmpty() && player.InvBody[INVLOC_HAND_LEFT]._itype != ItemType::Staff) || (!player.InvBody[INVLOC_HAND_RIGHT].isEmpty() && player.InvBody[INVLOC_HAND_RIGHT]._itype != ItemType::Staff)) player._pDamageMod /= 2; // Monks get half the normal damage bonus if they're holding a non-staff weapon } else if (player._pClass == HeroClass::Bard) { if (player.InvBody[INVLOC_HAND_LEFT]._itype == ItemType::Sword || player.InvBody[INVLOC_HAND_RIGHT]._itype == ItemType::Sword) - player._pDamageMod = player._pLevel * (player._pStrength + player._pDexterity) / 150; + player._pDamageMod = playerLevel * (player._pStrength + player._pDexterity) / 150; else if (player.InvBody[INVLOC_HAND_LEFT]._itype == ItemType::Bow || player.InvBody[INVLOC_HAND_RIGHT]._itype == ItemType::Bow) { - player._pDamageMod = player._pLevel * (player._pStrength + player._pDexterity) / 250; + player._pDamageMod = playerLevel * (player._pStrength + player._pDexterity) / 250; } else { - player._pDamageMod = player._pLevel * player._pStrength / 100; + player._pDamageMod = playerLevel * player._pStrength / 100; } } else if (player._pClass == HeroClass::Barbarian) { if (player.InvBody[INVLOC_HAND_LEFT]._itype == ItemType::Axe || player.InvBody[INVLOC_HAND_RIGHT]._itype == ItemType::Axe) { - player._pDamageMod = player._pLevel * player._pStrength / 75; + player._pDamageMod = playerLevel * player._pStrength / 75; } else if (player.InvBody[INVLOC_HAND_LEFT]._itype == ItemType::Mace || player.InvBody[INVLOC_HAND_RIGHT]._itype == ItemType::Mace) { - player._pDamageMod = player._pLevel * player._pStrength / 75; + player._pDamageMod = playerLevel * player._pStrength / 75; } else if (player.InvBody[INVLOC_HAND_LEFT]._itype == ItemType::Bow || player.InvBody[INVLOC_HAND_RIGHT]._itype == ItemType::Bow) { - player._pDamageMod = player._pLevel * player._pStrength / 300; + player._pDamageMod = playerLevel * player._pStrength / 300; } else { - player._pDamageMod = player._pLevel * player._pStrength / 100; + player._pDamageMod = playerLevel * player._pStrength / 100; } if (player.InvBody[INVLOC_HAND_LEFT]._itype == ItemType::Shield || player.InvBody[INVLOC_HAND_RIGHT]._itype == ItemType::Shield) { @@ -2708,11 +2710,11 @@ void CalcPlrItemVals(Player &player, bool loadgfx) else if (player.InvBody[INVLOC_HAND_RIGHT]._itype == ItemType::Shield) player._pIAC -= player.InvBody[INVLOC_HAND_RIGHT]._iAC / 2; } else if (IsNoneOf(player.InvBody[INVLOC_HAND_LEFT]._itype, ItemType::Staff, ItemType::Bow) && IsNoneOf(player.InvBody[INVLOC_HAND_RIGHT]._itype, ItemType::Staff, ItemType::Bow)) { - player._pDamageMod += player._pLevel * player._pVitality / 100; + player._pDamageMod += playerLevel * player._pVitality / 100; } - player._pIAC += player._pLevel / 4; + player._pIAC += playerLevel / 4; } else { - player._pDamageMod = player._pLevel * player._pStrength / 100; + player._pDamageMod = playerLevel * player._pStrength / 100; } player._pISpells = spl; @@ -2723,15 +2725,15 @@ void CalcPlrItemVals(Player &player, bool loadgfx) player._pIEnAc = enac; if (player._pClass == HeroClass::Barbarian) { - mr += player._pLevel; - fr += player._pLevel; - lr += player._pLevel; + mr += playerLevel; + fr += playerLevel; + lr += playerLevel; } if (HasAnyOf(player._pSpellFlags, SpellFlag::RageCooldown)) { - mr -= player._pLevel; - fr -= player._pLevel; - lr -= player._pLevel; + mr -= playerLevel; + fr -= playerLevel; + lr -= playerLevel; } if (HasAnyOf(iflgs, ItemSpecialEffect::ZeroResistance)) { @@ -2833,18 +2835,18 @@ void CalcPlrItemVals(Player &player, bool loadgfx) PlayerArmorGraphic animArmorId = PlayerArmorGraphic::Light; if (player.InvBody[INVLOC_CHEST]._itype == ItemType::HeavyArmor && player.InvBody[INVLOC_CHEST]._iStatFlag) { if (player._pClass == HeroClass::Monk && player.InvBody[INVLOC_CHEST]._iMagical == ITEM_QUALITY_UNIQUE) - player._pIAC += player._pLevel / 2; + player._pIAC += playerLevel / 2; animArmorId = PlayerArmorGraphic::Heavy; } else if (player.InvBody[INVLOC_CHEST]._itype == ItemType::MediumArmor && player.InvBody[INVLOC_CHEST]._iStatFlag) { if (player._pClass == HeroClass::Monk) { if (player.InvBody[INVLOC_CHEST]._iMagical == ITEM_QUALITY_UNIQUE) - player._pIAC += player._pLevel * 2; + player._pIAC += playerLevel * 2; else - player._pIAC += player._pLevel / 2; + player._pIAC += playerLevel / 2; } animArmorId = PlayerArmorGraphic::Medium; } else if (player._pClass == HeroClass::Monk) { - player._pIAC += player._pLevel * 2; + player._pIAC += playerLevel * 2; } const uint8_t gfxNum = static_cast(animWeaponId) | static_cast(animArmorId); @@ -3668,7 +3670,7 @@ void DoRepair(Player &player, int cii) pi = &player.InvBody[cii]; } - RepairItem(*pi, player._pLevel); + RepairItem(*pi, player.getCharacterLevel()); CalcPlrInv(player, true); } @@ -4241,7 +4243,7 @@ void SpawnSmith(int lvl) void SpawnPremium(const Player &player) { - int lvl = player._pLevel; + int lvl = player.getCharacterLevel(); int maxItems = gbIsHellfire ? SMITH_PREMIUM_ITEMS : 6; if (numpremium < maxItems) { for (int i = 0; i < maxItems; i++) { @@ -4842,7 +4844,7 @@ void RechargeItem(Item &item, Player &player) return; int r = GetSpellStaffLevel(item._iSpell); - r = GenerateRnd(player._pLevel / r) + 1; + r = GenerateRnd(player.getCharacterLevel() / r) + 1; do { item._iMaxCharges--; diff --git a/Source/levels/trigs.cpp b/Source/levels/trigs.cpp index 1e4319612..dc3f0e5bb 100644 --- a/Source/levels/trigs.cpp +++ b/Source/levels/trigs.cpp @@ -89,11 +89,11 @@ bool IsWarpOpen(dungeon_type type) return true; if (gbIsHellfire) { - if (type == DTYPE_CATACOMBS && myPlayer._pLevel >= 10) + if (type == DTYPE_CATACOMBS && myPlayer.getCharacterLevel() >= 10) return true; - if (type == DTYPE_CAVES && myPlayer._pLevel >= 15) + if (type == DTYPE_CAVES && myPlayer.getCharacterLevel() >= 15) return true; - if (type == DTYPE_HELL && myPlayer._pLevel >= 20) + if (type == DTYPE_HELL && myPlayer.getCharacterLevel() >= 20) return true; if (type == DTYPE_NEST && IsAnyOf(Quests[Q_FARMER]._qactive, QUEST_DONE, QUEST_HIVE_DONE)) return true; @@ -893,19 +893,19 @@ void CheckTriggers() diablo_message abortflag; auto position = myPlayer.position.tile; - if (trigs[i]._tlvl == 5 && myPlayer._pLevel < 8) { + if (trigs[i]._tlvl == 5 && myPlayer.getCharacterLevel() < 8) { abort = true; position.y += 1; abortflag = EMSG_REQUIRES_LVL_8; } - if (IsAnyOf(trigs[i]._tlvl, 9, 17) && myPlayer._pLevel < 13) { + if (IsAnyOf(trigs[i]._tlvl, 9, 17) && myPlayer.getCharacterLevel() < 13) { abort = true; position.x += 1; abortflag = EMSG_REQUIRES_LVL_13; } - if (IsAnyOf(trigs[i]._tlvl, 13, 21) && myPlayer._pLevel < 17) { + if (IsAnyOf(trigs[i]._tlvl, 13, 21) && myPlayer.getCharacterLevel() < 17) { abort = true; position.y += 1; abortflag = EMSG_REQUIRES_LVL_17; diff --git a/Source/loadsave.cpp b/Source/loadsave.cpp index ff5495b9c..27481b9ed 100644 --- a/Source/loadsave.cpp +++ b/Source/loadsave.cpp @@ -424,13 +424,13 @@ void LoadPlayer(LoadHelper &file, Player &player) player._pMaxManaBase = file.NextLE(); player._pMana = file.NextLE(); player._pMaxMana = file.NextLE(); - file.Skip(); // Skip _pManaPer - always derived from mana and maxMana - player._pLevel = file.NextLE(); - file.Skip(); // Skip _pMaxLevel - unused - file.Skip(2); // Alignment + file.Skip(); // Skip _pManaPer - always derived from mana and maxMana + player.setCharacterLevel(file.NextLE()); // this sets _pNextExper as well. + file.Skip(); // Skip _pMaxLevel - unused + file.Skip(2); // Alignment player._pExperience = file.NextLE(); - file.Skip(); // Skip _pMaxExp - unused - player._pNextExper = file.NextLE(); // This can be calculated based on _pLevel + file.Skip(); // Skip _pMaxExp - unused + file.Skip(); // Skip _pNextExper, it was calculated based on _pLevel above player._pArmorClass = file.NextLE(); player._pMagResist = file.NextLE(); player._pFireResist = file.NextLE(); @@ -1235,7 +1235,7 @@ void SavePlayer(SaveHelper &file, const Player &player) file.WriteLE(player._pMana); file.WriteLE(player._pMaxMana); file.Skip(); // Skip _pManaPer - file.WriteLE(player._pLevel); + file.WriteLE(player.getCharacterLevel()); file.Skip(); // skip _pMaxLevel, this value is uninitialised in most cases in Diablo/Hellfire so there's no point setting it. file.Skip(2); // Alignment file.WriteLE(player._pExperience); diff --git a/Source/missiles.cpp b/Source/missiles.cpp index 5a763b970..b99177349 100644 --- a/Source/missiles.cpp +++ b/Source/missiles.cpp @@ -333,7 +333,7 @@ bool Plr2PlrMHit(const Player &player, int p, int mindam, int maxdam, int dist, - target.GetArmor(); } else { hit = player.GetMagicToHit() - - (target._pLevel * 2) + - (target.getCharacterLevel() * 2) - dist; } @@ -348,7 +348,7 @@ bool Plr2PlrMHit(const Player &player, int p, int mindam, int maxdam, int dist, blkper = GenerateRnd(100); } - int blk = target.GetBlockChance() - (player._pLevel * 2); + int blk = target.GetBlockChance() - (player.getCharacterLevel() * 2); blk = std::clamp(blk, 0, 100); int dam; @@ -672,7 +672,7 @@ bool GuardianTryFireAt(Missile &missile, Point target) return false; Player &player = Players[missile._misource]; - int dmg = GenerateRnd(10) + (player._pLevel / 2) + 1; + int dmg = GenerateRnd(10) + (player.getCharacterLevel() / 2) + 1; dmg = ScaleSpellEffect(dmg, missile._mispllvl); Direction dir = GetDirection(position, target); @@ -795,14 +795,14 @@ DamageRange GetDamageAmt(SpellID spell, int spellLevel) case SpellID::HealOther: /// BUGFIX: healing calculation is unused return { - AddClassHealingBonus(myPlayer._pLevel + spellLevel + 1, myPlayer._pClass) - 1, - AddClassHealingBonus((4 * myPlayer._pLevel) + (6 * spellLevel) + 10, myPlayer._pClass) - 1 + AddClassHealingBonus(myPlayer.getCharacterLevel() + spellLevel + 1, myPlayer._pClass) - 1, + AddClassHealingBonus((4 * myPlayer.getCharacterLevel()) + (6 * spellLevel) + 10, myPlayer._pClass) - 1 }; case SpellID::RuneOfLight: case SpellID::Lightning: - return { 2, 2 + myPlayer._pLevel }; + return { 2, 2 + myPlayer.getCharacterLevel() }; case SpellID::Flash: { - int min = ScaleSpellEffect(myPlayer._pLevel, spellLevel); + int min = ScaleSpellEffect(myPlayer.getCharacterLevel(), spellLevel); min += min / 2; return { min, min * 2 }; }; @@ -833,28 +833,28 @@ DamageRange GetDamageAmt(SpellID spell, int spellLevel) case SpellID::FireWall: case SpellID::LightningWall: case SpellID::RingOfFire: { - const int min = 2 * myPlayer._pLevel + 4; + const int min = 2 * myPlayer.getCharacterLevel() + 4; return { min, min + 36 }; } case SpellID::Fireball: case SpellID::RuneOfFire: { - const int base = (2 * myPlayer._pLevel) + 4; + const int base = (2 * myPlayer.getCharacterLevel()) + 4; return { ScaleSpellEffect(base, spellLevel), ScaleSpellEffect(base + 36, spellLevel) }; } break; case SpellID::Guardian: { - const int base = (myPlayer._pLevel / 2) + 1; + const int base = (myPlayer.getCharacterLevel() / 2) + 1; return { ScaleSpellEffect(base, spellLevel), ScaleSpellEffect(base + 9, spellLevel) }; } break; case SpellID::ChainLightning: - return { 4, 4 + (2 * myPlayer._pLevel) }; + return { 4, 4 + (2 * myPlayer.getCharacterLevel()) }; case SpellID::FlameWave: { - const int min = 6 * (myPlayer._pLevel + 1); + const int min = 6 * (myPlayer.getCharacterLevel() + 1); return { min, min + 54 }; } case SpellID::Nova: @@ -862,28 +862,28 @@ DamageRange GetDamageAmt(SpellID spell, int spellLevel) case SpellID::RuneOfImmolation: case SpellID::RuneOfNova: return { - ScaleSpellEffect((myPlayer._pLevel + 5) / 2, spellLevel) * 5, - ScaleSpellEffect((myPlayer._pLevel + 30) / 2, spellLevel) * 5 + ScaleSpellEffect((myPlayer.getCharacterLevel() + 5) / 2, spellLevel) * 5, + ScaleSpellEffect((myPlayer.getCharacterLevel() + 30) / 2, spellLevel) * 5 }; case SpellID::Inferno: { - int max = myPlayer._pLevel + 4; + int max = myPlayer.getCharacterLevel() + 4; max += max / 2; return { 3, max }; } case SpellID::Golem: return { 11, 17 }; case SpellID::Apocalypse: - return { myPlayer._pLevel, myPlayer._pLevel * 6 }; + return { myPlayer.getCharacterLevel(), myPlayer.getCharacterLevel() * 6 }; case SpellID::Elemental: /// BUGFIX: Divide min and max by 2 return { - ScaleSpellEffect(2 * myPlayer._pLevel + 4, spellLevel), - ScaleSpellEffect(2 * myPlayer._pLevel + 40, spellLevel) + ScaleSpellEffect(2 * myPlayer.getCharacterLevel() + 4, spellLevel), + ScaleSpellEffect(2 * myPlayer.getCharacterLevel() + 40, spellLevel) }; case SpellID::ChargedBolt: return { 1, 1 + (myPlayer._pMagic / 4) }; case SpellID::HolyBolt: - return { myPlayer._pLevel + 9, myPlayer._pLevel + 18 }; + return { myPlayer.getCharacterLevel() + 9, myPlayer.getCharacterLevel() + 18 }; case SpellID::BloodStar: { const int min = (myPlayer._pMagic / 2) + 3 * spellLevel - (myPlayer._pMagic / 8); return { min, min }; @@ -999,14 +999,14 @@ bool PlayerMHit(int pnum, Monster *monster, int dist, int mind, int maxd, Missil int tac = player.GetArmor(); if (monster != nullptr) { hper = monster->toHit - + ((monster->level(sgGameInitInfo.nDifficulty) - player._pLevel) * 2) + + ((monster->level(sgGameInitInfo.nDifficulty) - player.getCharacterLevel()) * 2) + 30 - (dist * 2) - tac; } else { hper = 100 - (tac / 2) - (dist * 2); } } else if (monster != nullptr) { - hper += (monster->level(sgGameInitInfo.nDifficulty) * 2) - (player._pLevel * 2) - (dist * 2); + hper += (monster->level(sgGameInitInfo.nDifficulty) * 2) - (player.getCharacterLevel() * 2) - (dist * 2); } int minhit = 10; @@ -1030,7 +1030,7 @@ bool PlayerMHit(int pnum, Monster *monster, int dist, int mind, int maxd, Missil int blkper = player.GetBlockChance(false); if (monster != nullptr) - blkper -= (monster->level(sgGameInitInfo.nDifficulty) - player._pLevel) * 2; + blkper -= (monster->level(sgGameInitInfo.nDifficulty) - player.getCharacterLevel()) * 2; blkper = std::clamp(blkper, 0, 100); int8_t resper; @@ -1168,7 +1168,7 @@ void AddRuneOfFire(Missile &missile, AddMissileParameter ¶meter) void AddRuneOfLight(Missile &missile, AddMissileParameter ¶meter) { - int lvl = (missile.sourceType() == MissileSource::Player) ? missile.sourcePlayer()->_pLevel : 0; + int lvl = (missile.sourceType() == MissileSource::Player) ? missile.sourcePlayer()->getCharacterLevel() : 0; int dmg = 16 * (GenerateRndSum(10, 2) + lvl + 2); missile._midam = dmg; AddRune(missile, parameter.dst, MissileID::LightningWall); @@ -1198,7 +1198,7 @@ void AddReflect(Missile &missile, AddMissileParameter & /*parameter*/) Player &player = *missile.sourcePlayer(); - int add = (missile._mispllvl != 0 ? missile._mispllvl : 2) * player._pLevel; + int add = (missile._mispllvl != 0 ? missile._mispllvl : 2) * player.getCharacterLevel(); if (player.wReflections + add >= std::numeric_limits::max()) add = 0; player.wReflections += add; @@ -1399,9 +1399,9 @@ void AddSpectralArrow(Missile &missile, AddMissileParameter ¶meter) const Player &player = *missile.sourcePlayer(); if (player._pClass == HeroClass::Rogue) - av += (player._pLevel - 1) / 4; + av += (player.getCharacterLevel() - 1) / 4; else if (player._pClass == HeroClass::Warrior || player._pClass == HeroClass::Bard) - av += (player._pLevel - 1) / 8; + av += (player.getCharacterLevel() - 1) / 8; if (HasAnyOf(player._pIFlags, ItemSpecialEffect::QuickAttack)) av++; @@ -1518,7 +1518,7 @@ void AddLightningWall(Missile &missile, AddMissileParameter ¶meter) void AddBigExplosion(Missile &missile, AddMissileParameter & /*parameter*/) { if (missile.sourceType() == MissileSource::Player) { - int dmg = 2 * (missile.sourcePlayer()->_pLevel + GenerateRndSum(10, 2)) + 4; + int dmg = 2 * (missile.sourcePlayer()->getCharacterLevel() + GenerateRndSum(10, 2)) + 4; dmg = ScaleSpellEffect(dmg, missile._mispllvl); missile._midam = dmg; @@ -1572,7 +1572,7 @@ void AddMana(Missile &missile, AddMissileParameter & /*parameter*/) Player &player = Players[missile._misource]; int manaAmount = (GenerateRnd(10) + 1) << 6; - for (int i = 0; i < player._pLevel; i++) { + for (int i = 0; i < player.getCharacterLevel(); i++) { manaAmount += (GenerateRnd(4) + 1) << 6; } for (int i = 0; i < missile._mispllvl; i++) { @@ -1617,7 +1617,7 @@ void AddSearch(Missile &missile, AddMissileParameter & /*parameter*/) AutoMapShowItems = true; int lvl = 2; if (missile._misource >= 0) - lvl = player._pLevel * 2; + lvl = player.getCharacterLevel() * 2; missile._mirange = lvl + 10 * missile._mispllvl + 245; for (auto &other : Missiles) { @@ -1661,9 +1661,9 @@ void AddElementalArrow(Missile &missile, AddMissileParameter ¶meter) if (missile._micaster == TARGET_MONSTERS) { const Player &player = Players[missile._misource]; if (player._pClass == HeroClass::Rogue) - av += (player._pLevel) / 4; + av += (player.getCharacterLevel()) / 4; else if (IsAnyOf(player._pClass, HeroClass::Warrior, HeroClass::Bard)) - av += (player._pLevel) / 8; + av += (player.getCharacterLevel()) / 8; if (gbIsHellfire) { if (HasAnyOf(player._pIFlags, ItemSpecialEffect::QuickAttack)) @@ -1702,9 +1702,9 @@ void AddArrow(Missile &missile, AddMissileParameter ¶meter) av = GenerateRnd(32) + 16; } if (player._pClass == HeroClass::Rogue) - av += (player._pLevel - 1) / 4; + av += (player.getCharacterLevel() - 1) / 4; else if (player._pClass == HeroClass::Warrior || player._pClass == HeroClass::Bard) - av += (player._pLevel - 1) / 8; + av += (player.getCharacterLevel() - 1) / 8; if (gbIsHellfire) { if (HasAnyOf(player._pIFlags, ItemSpecialEffect::QuickAttack)) @@ -1873,7 +1873,7 @@ void AddNovaBall(Missile &missile, AddMissileParameter ¶meter) void AddFireWall(Missile &missile, AddMissileParameter ¶meter) { missile._midam = GenerateRndSum(10, 2) + 2; - missile._midam += missile._misource >= 0 ? Players[missile._misource]._pLevel : currlevel; // BUGFIX: missing parenthesis around ternary (fixed) + missile._midam += missile._misource >= 0 ? Players[missile._misource].getCharacterLevel() : currlevel; // BUGFIX: missing parenthesis around ternary (fixed) missile._midam <<= 3; UpdateMissileVelocity(missile, parameter.dst, 16); int i = missile._mispllvl; @@ -1897,7 +1897,7 @@ void AddFireball(Missile &missile, AddMissileParameter ¶meter) sp += std::min(missile._mispllvl * 2, 34); Player &player = Players[missile._misource]; - int dmg = 2 * (player._pLevel + GenerateRndSum(10, 2)) + 4; + int dmg = 2 * (player.getCharacterLevel() + GenerateRndSum(10, 2)) + 4; missile._midam = ScaleSpellEffect(dmg, missile._mispllvl); } UpdateMissileVelocity(missile, dst, sp); @@ -2035,7 +2035,7 @@ void AddFlashBottom(Missile &missile, AddMissileParameter & /*parameter*/) switch (missile.sourceType()) { case MissileSource::Player: { Player &player = *missile.sourcePlayer(); - int dmg = GenerateRndSum(20, player._pLevel + 1) + player._pLevel + 1; + int dmg = GenerateRndSum(20, player.getCharacterLevel() + 1) + player.getCharacterLevel() + 1; missile._midam = ScaleSpellEffect(dmg, missile._mispllvl); missile._midam += missile._midam / 2; } break; @@ -2054,7 +2054,7 @@ void AddFlashTop(Missile &missile, AddMissileParameter & /*parameter*/) { if (missile._micaster == TARGET_MONSTERS) { if (!missile.IsTrap()) { - int dmg = Players[missile._misource]._pLevel + 1; + int dmg = Players[missile._misource].getCharacterLevel() + 1; dmg += GenerateRndSum(20, dmg); missile._midam = ScaleSpellEffect(dmg, missile._mispllvl); missile._midam += missile._midam / 2; @@ -2084,7 +2084,7 @@ void AddManaShield(Missile &missile, AddMissileParameter ¶meter) void AddFlameWave(Missile &missile, AddMissileParameter ¶meter) { - missile._midam = GenerateRnd(10) + Players[missile._misource]._pLevel + 1; + missile._midam = GenerateRnd(10) + Players[missile._misource].getCharacterLevel() + 1; UpdateMissileVelocity(missile, parameter.dst, 16); missile._mirange = 255; @@ -2132,7 +2132,7 @@ void AddGuardian(Missile &missile, AddMissileParameter ¶meter) missile.position.start = *spawnPosition; missile._mlid = AddLight(missile.position.tile, 1); - missile._mirange = missile._mispllvl + (player._pLevel / 2); + missile._mirange = missile._mispllvl + (player.getCharacterLevel() / 2); if (missile._mirange > 30) missile._mirange = 30; @@ -2363,7 +2363,7 @@ void AddHealing(Missile &missile, AddMissileParameter & /*parameter*/) Player &player = Players[missile._misource]; int hp = GenerateRnd(10) + 1; - hp += GenerateRndSum(4, player._pLevel) + player._pLevel; + hp += GenerateRndSum(4, player.getCharacterLevel()) + player.getCharacterLevel(); hp += GenerateRndSum(6, missile._mispllvl) + missile._mispllvl; hp <<= 6; @@ -2401,7 +2401,7 @@ void AddElemental(Missile &missile, AddMissileParameter ¶meter) Player &player = Players[missile._misource]; - int dmg = 2 * (player._pLevel + GenerateRndSum(10, 2)) + 4; + int dmg = 2 * (player.getCharacterLevel() + GenerateRndSum(10, 2)) + 4; missile._midam = ScaleSpellEffect(dmg, missile._mispllvl) / 2; UpdateMissileVelocity(missile, dst, 16); @@ -2477,7 +2477,7 @@ void AddNova(Missile &missile, AddMissileParameter ¶meter) if (!missile.IsTrap()) { Player &player = Players[missile._misource]; - int dmg = GenerateRndSum(6, 5) + player._pLevel + 5; + int dmg = GenerateRndSum(6, 5) + player.getCharacterLevel() + 5; missile._midam = ScaleSpellEffect(dmg / 2, missile._mispllvl); } else { missile._midam = (currlevel / 2) + GenerateRndSum(3, 3); @@ -2490,17 +2490,17 @@ void AddRage(Missile &missile, AddMissileParameter ¶meter) { Player &player = Players[missile._misource]; - if (HasAnyOf(player._pSpellFlags, SpellFlag::RageActive | SpellFlag::RageCooldown) || player._pHitPoints <= player._pLevel << 6) { + if (HasAnyOf(player._pSpellFlags, SpellFlag::RageActive | SpellFlag::RageCooldown) || player._pHitPoints <= player.getCharacterLevel() << 6) { missile._miDelFlag = true; parameter.spellFizzled = true; return; } - int tmp = 3 * player._pLevel; + int tmp = 3 * player.getCharacterLevel(); tmp <<= 7; player._pSpellFlags |= SpellFlag::RageActive; missile.var2 = tmp; - int lvl = player._pLevel * 2; + int lvl = player.getCharacterLevel() * 2; missile._mirange = lvl + 10 * missile._mispllvl + 245; CalcPlrItemVals(player, true); RedrawEverything(); @@ -2567,7 +2567,7 @@ void AddApocalypse(Missile &missile, AddMissileParameter & /*parameter*/) missile.var4 = std::max(missile.position.start.x - 8, 1); missile.var5 = std::min(missile.position.start.x + 8, MAXDUNX - 1); missile.var6 = missile.var4; - int playerLevel = player._pLevel; + int playerLevel = player.getCharacterLevel(); missile._midam = GenerateRndSum(6, playerLevel) + playerLevel; missile._mirange = 255; } @@ -2582,7 +2582,7 @@ void AddInferno(Missile &missile, AddMissileParameter ¶meter) missile._mirange = missile.var2 + 20; missile._mlid = AddLight(missile.position.start, 1); if (missile._micaster == TARGET_MONSTERS) { - int i = GenerateRnd(Players[missile._misource]._pLevel) + GenerateRnd(2); + int i = GenerateRnd(Players[missile._misource].getCharacterLevel()) + GenerateRnd(2); missile._midam = 8 * i + 16 + ((8 * i + 16) / 2); } else { auto &monster = Monsters[missile._misource]; @@ -2639,7 +2639,7 @@ void AddHolyBolt(Missile &missile, AddMissileParameter ¶meter) missile.var1 = missile.position.start.x; missile.var2 = missile.position.start.y; missile._mlid = AddLight(missile.position.start, 8); - missile._midam = GenerateRnd(10) + player._pLevel + 9; + missile._midam = GenerateRnd(10) + player.getCharacterLevel() + 9; } void AddResurrect(Missile &missile, AddMissileParameter & /*parameter*/) @@ -3139,7 +3139,7 @@ void ProcessRingOfFire(Missile &missile) { missile._miDelFlag = true; int8_t src = missile._misource; - uint8_t lvl = missile._micaster == TARGET_MONSTERS ? Players[src]._pLevel : currlevel; + uint8_t lvl = missile._micaster == TARGET_MONSTERS ? Players[src].getCharacterLevel() : currlevel; int dmg = 16 * (GenerateRndSum(10, 2) + lvl + 2) / 2; if (missile.limitReached) @@ -3189,7 +3189,7 @@ void ProcessLightningWallControl(Missile &missile) } int id = missile._misource; - int lvl = !missile.IsTrap() ? Players[id]._pLevel : 0; + int lvl = !missile.IsTrap() ? Players[id].getCharacterLevel() : 0; int dmg = 16 * (GenerateRndSum(10, 2) + lvl + 2); { @@ -3301,7 +3301,7 @@ void ProcessLightningControl(Missile &missile) dam = GenerateRnd(currlevel) + 2 * currlevel; } else if (missile._micaster == TARGET_MONSTERS) { // BUGFIX: damage of missile should be encoded in missile struct; player can be dead/have left the game before missile arrives. - dam = (GenerateRnd(2) + GenerateRnd(Players[missile._misource]._pLevel) + 2) << 6; + dam = (GenerateRnd(2) + GenerateRnd(Players[missile._misource].getCharacterLevel()) + 2) << 6; } else { auto &monster = Monsters[missile._misource]; dam = 2 * (monster.minDamage + GenerateRnd(monster.maxDamage - monster.minDamage + 1)); @@ -3857,7 +3857,7 @@ void ProcessRage(Missile &missile) if (HasAnyOf(player._pSpellFlags, SpellFlag::RageActive)) { player._pSpellFlags &= ~SpellFlag::RageActive; player._pSpellFlags |= SpellFlag::RageCooldown; - int lvl = player._pLevel * 2; + int lvl = player.getCharacterLevel() * 2; missile._mirange = lvl + 10 * missile._mispllvl + 245; } else { player._pSpellFlags &= ~SpellFlag::RageCooldown; diff --git a/Source/monster.cpp b/Source/monster.cpp index 90fcb9d2f..08999f6c0 100644 --- a/Source/monster.cpp +++ b/Source/monster.cpp @@ -1159,7 +1159,7 @@ void MonsterAttackPlayer(Monster &monster, Player &player, int hit, int minDam, ac += 40; if (HasAnyOf(player.pDamAcFlags, ItemSpecialEffectHf::ACAgainstUndead) && monster.data().monsterClass == MonsterClass::Undead) ac += 20; - hit += 2 * (monster.level(sgGameInitInfo.nDifficulty) - player._pLevel) + hit += 2 * (monster.level(sgGameInitInfo.nDifficulty) - player.getCharacterLevel()) + 30 - ac; int minhit = GetMinHit(); @@ -4571,7 +4571,7 @@ void SpawnGolem(Player &player, Monster &golem, Point position, Missile &missile golem.maxHitPoints = 2 * (320 * missile._mispllvl + player._pMaxMana / 3); golem.hitPoints = golem.maxHitPoints; golem.armorClass = 25; - golem.toHit = 5 * (missile._mispllvl + 8) + 2 * player._pLevel; + golem.toHit = 5 * (missile._mispllvl + 8) + 2 * player.getCharacterLevel(); golem.minDamage = 2 * (missile._mispllvl + 4); golem.maxDamage = 2 * (missile._mispllvl + 8); golem.flags |= MFLAG_GOLEM; diff --git a/Source/msg.cpp b/Source/msg.cpp index 0334c189a..e3e19cfe5 100644 --- a/Source/msg.cpp +++ b/Source/msg.cpp @@ -1951,7 +1951,7 @@ size_t OnPlayerLevel(const TCmd *pCmd, size_t pnum) if (gbBufferMsgs != 1) { Player &player = Players[pnum]; if (playerLevel <= MaxCharacterLevel && &player != MyPlayer) - player._pLevel = static_cast(playerLevel); + player.setCharacterLevel(static_cast(playerLevel)); } else { SendPacket(pnum, &message, sizeof(message)); } @@ -2026,7 +2026,7 @@ size_t OnPlayerJoinLevel(const TCmd *pCmd, size_t pnum) ResetPlayerGFX(player); player.plractive = true; gbActivePlayers++; - EventPlrMsg(fmt::format(fmt::runtime(_("Player '{:s}' (level {:d}) just joined the game")), player._pName, player._pLevel)); + EventPlrMsg(fmt::format(fmt::runtime(_("Player '{:s}' (level {:d}) just joined the game")), player._pName, player.getCharacterLevel())); } if (player.plractive && &player != MyPlayer) { @@ -2225,7 +2225,7 @@ size_t OnCheatExperience(const TCmd *pCmd, size_t pnum) // NOLINT(misc-unused-pa #ifdef _DEBUG if (gbBufferMsgs == 1) SendPacket(pnum, pCmd, sizeof(*pCmd)); - else if (Players[pnum]._pLevel < MaxCharacterLevel) { + else if (Players[pnum].getCharacterLevel() < MaxCharacterLevel) { Players[pnum]._pExperience = Players[pnum]._pNextExper; if (*sgOptions.Gameplay.experienceBar) { RedrawEverything(); diff --git a/Source/multi.cpp b/Source/multi.cpp index b770e1610..498bee7f5 100644 --- a/Source/multi.cpp +++ b/Source/multi.cpp @@ -153,9 +153,9 @@ void NetReceivePlayerData(TPkt *pkt) bool IsNetPlayerValid(const Player &player) { - return player._pLevel >= 1 - && player._pLevel <= MaxCharacterLevel - && static_cast(player._pClass) < enum_size::value + // we no longer check character level here, players with out-of-range clevels are not allowed to join the game and we don't observe change clevel messages that would set it out of range + // (there's no code path that would result in _pLevel containing an out of range value in the DevilutionX code) + return static_cast(player._pClass) < enum_size::value && player.plrlevel < NUMLEVELS && InDungeonBounds(player.position.tile) && !std::string_view(player._pName).empty(); @@ -794,7 +794,7 @@ bool NetInit(bool bSinglePlayer) Player &myPlayer = *MyPlayer; // separator for marking messages from a different game AddMessageToChatLog(_("New Game"), nullptr, UiFlags::ColorRed); - AddMessageToChatLog(fmt::format(fmt::runtime(_("Player '{:s}' (level {:d}) just joined the game")), myPlayer._pName, myPlayer._pLevel)); + AddMessageToChatLog(fmt::format(fmt::runtime(_("Player '{:s}' (level {:d}) just joined the game")), myPlayer._pName, myPlayer.getCharacterLevel())); return true; } @@ -849,7 +849,7 @@ void recv_plrinfo(int pnum, const TCmdPlrInfoHdr &header, bool recv) } else { szEvent = _("Player '{:s}' (level {:d}) is already in the game"); } - EventPlrMsg(fmt::format(fmt::runtime(szEvent), player._pName, player._pLevel)); + EventPlrMsg(fmt::format(fmt::runtime(szEvent), player._pName, player.getCharacterLevel())); SyncInitPlr(player); diff --git a/Source/objects.cpp b/Source/objects.cpp index c0e19b2d6..42c694e10 100644 --- a/Source/objects.cpp +++ b/Source/objects.cpp @@ -2850,7 +2850,7 @@ void OperateShrineMendicant(Player &player) return; int gold = player._pGold / 2; - AddPlrExperience(player, player._pLevel, gold); + AddPlrExperience(player, player.getCharacterLevel(), gold); TakePlrsMoney(gold); RedrawEverything(); @@ -2868,7 +2868,7 @@ void OperateShrineSparkling(Player &player, Point spawnPosition) if (&player != MyPlayer) return; - AddPlrExperience(player, player._pLevel, 1000 * currlevel); + AddPlrExperience(player, player.getCharacterLevel(), 1000 * currlevel); AddMissile( spawnPosition, diff --git a/Source/pack.cpp b/Source/pack.cpp index 242ee6999..837770d07 100644 --- a/Source/pack.cpp +++ b/Source/pack.cpp @@ -255,7 +255,7 @@ void PackPlayer(PlayerPack &packed, const Player &player) packed.pBaseMag = player._pBaseMag; packed.pBaseDex = player._pBaseDex; packed.pBaseVit = player._pBaseVit; - packed.pLevel = player._pLevel; + packed.pLevel = player.getCharacterLevel(); packed.pStatPts = player._pStatPts; packed.pExperience = SDL_SwapLE32(player._pExperience); packed.pGold = SDL_SwapLE32(player._pGold); @@ -300,7 +300,7 @@ void PackNetPlayer(PlayerNetPack &packed, const Player &player) packed.pBaseMag = player._pBaseMag; packed.pBaseDex = player._pBaseDex; packed.pBaseVit = player._pBaseVit; - packed.pLevel = player._pLevel; + packed.pLevel = player.getCharacterLevel(); packed.pStatPts = player._pStatPts; packed.pExperience = SDL_SwapLE32(player._pExperience); packed.pHPBase = SDL_SwapLE32(player._pHPBase); @@ -421,7 +421,7 @@ void UnPackPlayer(const PlayerPack &packed, Player &player) Point position { packed.px, packed.py }; player = {}; - player._pLevel = std::clamp(packed.pLevel, 1, MaxCharacterLevel); + player.setCharacterLevel(packed.pLevel); player._pMaxHPBase = SDL_SwapLE32(packed.pMaxHPBase); player._pHPBase = SDL_SwapLE32(packed.pHPBase); player._pHPBase = std::clamp(player._pHPBase, 0, player._pMaxHPBase); @@ -515,7 +515,7 @@ bool UnPackNetPlayer(const PlayerNetPack &packed, Player &player) ValidateField(packed._pNumInv, packed._pNumInv < InventoryGridCells); - player._pLevel = packed.pLevel; + player.setCharacterLevel(packed.pLevel); player.position.tile = position; player.position.future = position; player.plrlevel = packed.plrlevel; diff --git a/Source/panels/charpanel.cpp b/Source/panels/charpanel.cpp index cdf67290e..bb94fb779 100644 --- a/Source/panels/charpanel.cpp +++ b/Source/panels/charpanel.cpp @@ -126,7 +126,7 @@ PanelEntry panelEntries[] = { []() { return StyledText { UiFlags::ColorWhite, std::string(_(PlayersData[static_cast(InspectPlayer->_pClass)].className)) }; } }, { N_("Level"), { 57, 52 }, 57, 45, - []() { return StyledText { UiFlags::ColorWhite, StrCat(InspectPlayer->_pLevel) }; } }, + []() { return StyledText { UiFlags::ColorWhite, StrCat(InspectPlayer->getCharacterLevel()) }; } }, { N_("Experience"), { TopRightLabelX, 52 }, 99, 91, []() { int spacing = ((InspectPlayer->_pExperience >= 1000000000) ? 0 : 1); @@ -134,7 +134,7 @@ PanelEntry panelEntries[] = { } }, { N_("Next level"), { TopRightLabelX, 80 }, 99, 198, []() { - if (InspectPlayer->_pLevel == MaxCharacterLevel) { + if (InspectPlayer->getCharacterLevel() == MaxCharacterLevel) { return StyledText { UiFlags::ColorWhitegold, std::string(_("None")) }; } int spacing = ((InspectPlayer->_pNextExper >= 1000000000) ? 0 : 1); @@ -168,7 +168,7 @@ PanelEntry panelEntries[] = { []() { return StyledText { UiFlags::ColorWhite, FormatInteger(InspectPlayer->_pGold) }; } }, { N_("Armor class"), { RightColumnLabelX, 163 }, 57, RightColumnLabelWidth, - []() { return StyledText { GetValueColor(InspectPlayer->_pIBonusAC), StrCat(InspectPlayer->GetArmor() + InspectPlayer->_pLevel * 2) }; } }, + []() { return StyledText { GetValueColor(InspectPlayer->_pIBonusAC), StrCat(InspectPlayer->GetArmor() + InspectPlayer->getCharacterLevel() * 2) }; } }, { N_("To hit"), { RightColumnLabelX, 191 }, 57, RightColumnLabelWidth, []() { return StyledText { GetValueColor(InspectPlayer->_pIBonusToHit), StrCat(InspectPlayer->InvBody[INVLOC_HAND_LEFT]._itype == ItemType::Bow ? InspectPlayer->GetRangedToHit() : InspectPlayer->GetMeleeToHit(), "%") }; } }, { N_("Damage"), { RightColumnLabelX, 219 }, 57, RightColumnLabelWidth, diff --git a/Source/pfile.cpp b/Source/pfile.cpp index 07a747765..813e82690 100644 --- a/Source/pfile.cpp +++ b/Source/pfile.cpp @@ -171,7 +171,7 @@ void CopySaveFile(uint32_t saveNum, std::string targetPath) void Game2UiPlayer(const Player &player, _uiheroinfo *heroinfo, bool bHasSaveFile) { CopyUtf8(heroinfo->name, player._pName, sizeof(heroinfo->name)); - heroinfo->level = player._pLevel; + heroinfo->level = player.getCharacterLevel(); heroinfo->heroclass = player._pClass; heroinfo->strength = player._pStrength; heroinfo->magic = player._pMagic; diff --git a/Source/player.cpp b/Source/player.cpp index 29a601191..c1da74db5 100644 --- a/Source/player.cpp +++ b/Source/player.cpp @@ -578,10 +578,10 @@ bool PlrHitMonst(Player &player, Monster &monster, bool adjacentDamage = false) return false; if (adjacentDamage) { - if (player._pLevel > 20) + if (player.getCharacterLevel() > 20) hper -= 30; else - hper -= (35 - player._pLevel) * 2; + hper -= (35 - player.getCharacterLevel()) * 2; } int hit = GenerateRnd(100); @@ -614,7 +614,7 @@ bool PlrHitMonst(Player &player, Monster &monster, bool adjacentDamage = false) int dam2 = dam << 6; dam += player._pDamageMod; if (player._pClass == HeroClass::Warrior || player._pClass == HeroClass::Barbarian) { - if (GenerateRnd(100) < player._pLevel) { + if (GenerateRnd(100) < player.getCharacterLevel()) { dam *= 2; } } @@ -761,7 +761,7 @@ bool PlrHitPlr(Player &attacker, Player &target) blk = GenerateRnd(100); } - int blkper = target.GetBlockChance() - (attacker._pLevel * 2); + int blkper = target.GetBlockChance() - (attacker.getCharacterLevel() * 2); blkper = std::clamp(blkper, 0, 100); if (hit >= hper) { @@ -781,7 +781,7 @@ bool PlrHitPlr(Player &attacker, Player &target) dam += attacker._pIBonusDamMod + attacker._pDamageMod; if (attacker._pClass == HeroClass::Warrior || attacker._pClass == HeroClass::Barbarian) { - if (GenerateRnd(100) < attacker._pLevel) { + if (GenerateRnd(100) < attacker.getCharacterLevel()) { dam *= 2; } } @@ -1474,8 +1474,9 @@ void ValidatePlayer() assert(MyPlayer != nullptr); Player &myPlayer = *MyPlayer; - if (myPlayer._pLevel > MaxCharacterLevel) - myPlayer._pLevel = MaxCharacterLevel; + // Player::setCharacterLevel both ensures that the player level is within the expected range and sets _pNextExpr to the appropriate value for their next level up + myPlayer.setCharacterLevel(myPlayer.getCharacterLevel()); + // This lets us catch cases where someone is editing experience directly through memory modification and reset their experience back to the expected cap. if (myPlayer._pExperience > myPlayer._pNextExper) { myPlayer._pExperience = myPlayer._pNextExper; if (*sgOptions.Gameplay.experienceBar) { @@ -2056,16 +2057,22 @@ void Player::UpdatePreviewCelSprite(_cmd_id cmdId, Point point, uint16_t wParam1 } } +void Player::setCharacterLevel(uint8_t level) +{ + this->_pLevel = std::clamp(level, 1U, MaxCharacterLevel); + this->_pNextExper = GetNextExperienceThresholdForLevel(this->getCharacterLevel()); +} + int32_t Player::calculateBaseLife() const { const PlayerData &playerData = PlayersData[static_cast(_pClass)]; - return playerData.adjLife + (playerData.lvlLife * _pLevel) + (playerData.chrLife * _pBaseVit); + return playerData.adjLife + (playerData.lvlLife * getCharacterLevel()) + (playerData.chrLife * _pBaseVit); } int32_t Player::calculateBaseMana() const { const PlayerData &playerData = PlayersData[static_cast(_pClass)]; - return playerData.adjMana + (playerData.lvlMana * _pLevel) + (playerData.chrMana * _pBaseMag); + return playerData.adjMana + (playerData.lvlMana * getCharacterLevel()) + (playerData.chrMana * _pBaseMag); } Player *PlayerAtPosition(Point position) @@ -2280,7 +2287,7 @@ void CreatePlayer(Player &player, HeroClass c) const PlayerData &playerData = PlayersData[static_cast(c)]; - player._pLevel = 1; + player.setCharacterLevel(1); player._pClass = c; player._pBaseStr = playerData.baseStr; @@ -2308,7 +2315,6 @@ void CreatePlayer(Player &player, HeroClass c) player._pMaxManaBase = player._pMana; player._pExperience = 0; - player._pNextExper = GetNextExperienceThresholdForLevel(player._pLevel); player._pArmorClass = 0; player._pLightRad = 10; player._pInfraFlag = false; @@ -2388,7 +2394,7 @@ int CalcStatDiff(Player &player) void NextPlrLevel(Player &player) { - player._pLevel++; + player.setCharacterLevel(player.getCharacterLevel() + 1); CalcPlrInv(player, true); @@ -2397,8 +2403,6 @@ void NextPlrLevel(Player &player) } else { player._pStatPts += 5; } - player._pNextExper = GetNextExperienceThresholdForLevel(player._pLevel); - int hp = PlayersData[static_cast(player._pClass)].lvlLife; player._pMaxHP += hp; @@ -2437,22 +2441,18 @@ void AddPlrExperience(Player &player, int lvl, int exp) if (&player != MyPlayer || player._pHitPoints <= 0) return; - if (player._pLevel >= MaxCharacterLevel) { - player._pLevel = MaxCharacterLevel; + if (player.getCharacterLevel() >= MaxCharacterLevel) { return; } // Adjust xp based on difference in level between player and monster - uint32_t clampedExp = std::max(static_cast(exp * (1 + (lvl - player._pLevel) / 10.0)), 0); + uint32_t clampedExp = std::max(static_cast(exp * (1 + (lvl - player.getCharacterLevel()) / 10.0)), 0); // Prevent power leveling if (gbIsMultiplayer) { - // Use a minimum of 1 so level 0 characters can still gain experience - const uint32_t clampedPlayerLevel = std::max(player._pLevel, 1); - // for low level characters experience gain is capped to 1/20 of current levels xp // for high level characters experience gain is capped to 200 * current level - this is a smaller value than 1/20 of the exp needed for the next level after level 5. - clampedExp = std::min({ clampedExp, /* level 1-5: */ GetNextExperienceThresholdForLevel(clampedPlayerLevel) / 20U, /* level 6-50: */ 200U * clampedPlayerLevel }); + clampedExp = std::min({ clampedExp, /* level 1-5: */ GetNextExperienceThresholdForLevel(player.getCharacterLevel()) / 20U, /* level 6-50: */ 200U * player.getCharacterLevel() }); } const uint32_t MaxExperience = GetNextExperienceThresholdForLevel(MaxCharacterLevel); @@ -2465,17 +2465,17 @@ void AddPlrExperience(Player &player, int lvl, int exp) } // Increase player level if applicable - unsigned newLvl = player._pLevel; + unsigned newLvl = player.getCharacterLevel(); while (newLvl < MaxCharacterLevel && player._pExperience >= GetNextExperienceThresholdForLevel(newLvl)) { newLvl++; } - if (newLvl != player._pLevel) { - for (unsigned i = newLvl - player._pLevel; i > 0; i--) { + if (newLvl != player.getCharacterLevel()) { + for (unsigned i = newLvl - player.getCharacterLevel(); i > 0; i--) { NextPlrLevel(player); } } - NetSendCmdParam1(false, CMD_PLRLEVEL, player._pLevel); + NetSendCmdParam1(false, CMD_PLRLEVEL, player.getCharacterLevel()); } void AddPlrMonstExper(int lvl, int exp, char pmask) @@ -2548,7 +2548,6 @@ void InitPlayer(Player &player, bool firstTime) SpellID s = PlayersData[static_cast(player._pClass)].skill; player._pAblSpells = GetSpellBitmask(s); - player._pNextExper = GetNextExperienceThresholdForLevel(player._pLevel); player._pInvincible = false; if (&player == MyPlayer) { @@ -2663,10 +2662,10 @@ void StartPlrHit(Player &player, int dam, bool forcehit) RedrawComponent(PanelDrawComponent::Health); if (player._pClass == HeroClass::Barbarian) { - if (dam >> 6 < player._pLevel + player._pLevel / 4 && !forcehit) { + if (dam >> 6 < player.getCharacterLevel() + player.getCharacterLevel() / 4 && !forcehit) { return; } - } else if (dam >> 6 < player._pLevel && !forcehit) { + } else if (dam >> 6 < player.getCharacterLevel() && !forcehit) { return; } @@ -2781,7 +2780,7 @@ StartPlayerKill(Player &player, DeathReason deathReason) ear._iCreateInfo = player._pName[0] << 8 | player._pName[1]; ear._iSeed = player._pName[2] << 24 | player._pName[3] << 16 | player._pName[4] << 8 | player._pName[5]; - ear._ivalue = player._pLevel; + ear._ivalue = player.getCharacterLevel(); if (FindGetItem(ear._iSeed, IDI_EAR, ear._iCreateInfo) == -1) { DeadItem(player, std::move(ear), { 0, 0 }); diff --git a/Source/player.h b/Source/player.h index bfc3473bd..2349c7939 100644 --- a/Source/player.h +++ b/Source/player.h @@ -318,7 +318,11 @@ struct Player { ActorPosition position; Direction _pdir; // Direction faced by player (direction enum) HeroClass _pClass; - uint8_t _pLevel; + +private: + uint8_t _pLevel = 1; // Use get/setCharacterLevel as this attribute is tied to _pNextExper + +public: uint8_t _pgfxnum; // Bitmask indicating what variant of the sprite the player is using. The 3 lower bits define weapon (PlayerWeaponGraphic) and the higher bits define armour (starting with PlayerArmorGraphic) int8_t _pISplLvlAdd; /** @brief Specifies whether players are in non-PvP mode. */ @@ -510,7 +514,7 @@ struct Player { */ int GetMeleeToHit() const { - int hper = _pLevel + _pDexterity / 2 + _pIBonusToHit + BaseHitChance; + int hper = getCharacterLevel() + _pDexterity / 2 + _pIBonusToHit + BaseHitChance; if (_pClass == HeroClass::Warrior) hper += 20; return hper; @@ -533,7 +537,7 @@ struct Player { */ int GetRangedToHit() const { - int hper = _pLevel + _pDexterity + _pIBonusToHit + BaseHitChance; + int hper = getCharacterLevel() + _pDexterity + _pIBonusToHit + BaseHitChance; if (_pClass == HeroClass::Rogue) hper += 20; else if (_pClass == HeroClass::Warrior || _pClass == HeroClass::Bard) @@ -571,7 +575,7 @@ struct Player { { int blkper = _pDexterity + _pBaseToBlk; if (useLevel) - blkper += _pLevel * 2; + blkper += getCharacterLevel() * 2; return blkper; } @@ -749,6 +753,20 @@ struct Player { */ void UpdatePreviewCelSprite(_cmd_id cmdId, Point point, uint16_t wParam1, uint16_t wParam2); + [[nodiscard]] uint8_t getCharacterLevel() const + { + return _pLevel; + } + + /** + * @brief Sets the character level and derived attributes + * + * This method ensures the level is within the allowed range and sets the number of experience points + * required for the next character level as needed. + * @param level New character level + */ + void setCharacterLevel(uint8_t level); + /** @brief Checks if the player is on the same level as the local player (MyPlayer). */ bool isOnActiveLevel() const { diff --git a/Source/plrmsg.cpp b/Source/plrmsg.cpp index 7d3986082..83c579719 100644 --- a/Source/plrmsg.cpp +++ b/Source/plrmsg.cpp @@ -82,7 +82,7 @@ void SendPlrMsg(Player &player, std::string_view text) { PlayerMessage &message = GetNextMessage(); - std::string from = fmt::format(fmt::runtime(_("{:s} (lvl {:d}): ")), player._pName, player._pLevel); + std::string from = fmt::format(fmt::runtime(_("{:s} (lvl {:d}): ")), player._pName, player.getCharacterLevel()); message.style = UiFlags::ColorWhite; message.time = SDL_GetTicks(); diff --git a/Source/qol/chatlog.cpp b/Source/qol/chatlog.cpp index 1c778a78c..10d725f53 100644 --- a/Source/qol/chatlog.cpp +++ b/Source/qol/chatlog.cpp @@ -127,7 +127,7 @@ void AddMessageToChatLog(std::string_view message, Player *player, UiFlags flags if (player == nullptr) { ChatLogLines.emplace_back(MultiColoredText { "{0} {1}", { { timestamp, UiFlags::ColorRed }, { std::string(message), flags } } }); } else { - std::string playerInfo = fmt::format(fmt::runtime(_("{:s} (lvl {:d}): ")), player->_pName, player->_pLevel); + std::string playerInfo = fmt::format(fmt::runtime(_("{:s} (lvl {:d}): ")), player->_pName, player->getCharacterLevel()); ChatLogLines.emplace_back(MultiColoredText { std::string(message), { {} }, 20 }); UiFlags nameColor = player == MyPlayer ? UiFlags::ColorWhitegold : UiFlags::ColorBlue; ChatLogLines.emplace_back(MultiColoredText { "{0} - {1}", { { timestamp, UiFlags::ColorRed }, { playerInfo, nameColor } } }); diff --git a/Source/qol/xpbar.cpp b/Source/qol/xpbar.cpp index 5c7e1a4c3..d489c4b5a 100644 --- a/Source/qol/xpbar.cpp +++ b/Source/qol/xpbar.cpp @@ -76,7 +76,7 @@ void DrawXPBar(const Surface &out) RenderClxSprite(out, (*xpbarArt)[0], back); - const uint8_t charLevel = player._pLevel; + const uint8_t charLevel = player.getCharacterLevel(); if (charLevel == MaxCharacterLevel) { // Draw a nice golden bar for max level characters. @@ -120,7 +120,7 @@ bool CheckXPBarInfo() const Player &player = *MyPlayer; - const uint8_t charLevel = player._pLevel; + const uint8_t charLevel = player.getCharacterLevel(); AddPanelString(fmt::format(fmt::runtime(_("Level {:d}")), charLevel)); @@ -137,8 +137,8 @@ bool CheckXPBarInfo() InfoColor = UiFlags::ColorWhite; AddPanelString(fmt::format(fmt::runtime(_("Experience: {:s}")), FormatInteger(player._pExperience))); - AddPanelString(fmt::format(fmt::runtime(_("Next Level: {:s}")), FormatInteger(GetNextExperienceThresholdForLevel(charLevel)))); - AddPanelString(fmt::format(fmt::runtime(_("{:s} to Level {:d}")), FormatInteger(GetNextExperienceThresholdForLevel(charLevel) - player._pExperience), charLevel + 1)); + AddPanelString(fmt::format(fmt::runtime(_("Next Level: {:s}")), FormatInteger(player._pNextExper))); + AddPanelString(fmt::format(fmt::runtime(_("{:s} to Level {:d}")), FormatInteger(player._pNextExper - player._pExperience), charLevel + 1)); return true; } diff --git a/Source/spells.cpp b/Source/spells.cpp index 28a42413e..d76846ef1 100644 --- a/Source/spells.cpp +++ b/Source/spells.cpp @@ -121,7 +121,7 @@ int GetManaAmount(const Player &player, SpellID sn) } if (sn == SpellID::Healing || sn == SpellID::HealOther) { - ma = (GetSpellData(SpellID::Healing).sManaCost + 2 * player._pLevel - adj); + ma = (GetSpellData(SpellID::Healing).sManaCost + 2 * player.getCharacterLevel() - adj); } else if (GetSpellData(sn).sManaCost == 255) { ma = (player._pMaxManaBase >> 6) - adj; } else { @@ -282,7 +282,7 @@ void DoHealOther(const Player &caster, Player &target) } int hp = (GenerateRnd(10) + 1) << 6; - for (unsigned i = 0; i < caster._pLevel; i++) { + for (unsigned i = 0; i < caster.getCharacterLevel(); i++) { hp += (GenerateRnd(4) + 1) << 6; } for (int i = 0; i < caster.GetSpellLevel(SpellID::HealOther); i++) { diff --git a/Source/stores.cpp b/Source/stores.cpp index ee4f81ffb..bae307456 100644 --- a/Source/stores.cpp +++ b/Source/stores.cpp @@ -2124,7 +2124,7 @@ void SetupTownStores() { Player &myPlayer = *MyPlayer; - int l = myPlayer._pLevel / 2; + int l = myPlayer.getCharacterLevel() / 2; if (!gbIsMultiplayer) { l = 0; for (int i = 0; i < NUMLEVELS; i++) { @@ -2139,7 +2139,7 @@ void SetupTownStores() SpawnSmith(l); SpawnWitch(l); SpawnHealer(l); - SpawnBoy(myPlayer._pLevel); + SpawnBoy(myPlayer.getCharacterLevel()); SpawnPremium(myPlayer); } diff --git a/Source/towners.cpp b/Source/towners.cpp index 37cd6779a..6e91900ce 100644 --- a/Source/towners.cpp +++ b/Source/towners.cpp @@ -622,7 +622,7 @@ void TalkToFarmer(Player &player, Towner &farmer) break; } - if (!player._pLvlVisited[9] && player._pLevel < 15) { + if (!player._pLvlVisited[9] && player.getCharacterLevel() < 15) { _speech_id qt = TEXT_FARMER8; if (player._pLvlVisited[2]) qt = TEXT_FARMER5; @@ -713,7 +713,7 @@ void TalkToCowFarmer(Player &player, Towner &cowFarmer) NetSendCmdQuest(true, quest); break; case QUEST_HIVE_ACTIVE: - if (!player._pLvlVisited[9] && player._pLevel < 15) { + if (!player._pLvlVisited[9] && player.getCharacterLevel() < 15) { _speech_id qt = TEXT_JERSEY12; switch (GenerateRnd(4)) { case 0: diff --git a/test/player_test.cpp b/test/player_test.cpp index a92baa729..6c0d9e777 100644 --- a/test/player_test.cpp +++ b/test/player_test.cpp @@ -14,7 +14,9 @@ int RunBlockTest(int frames, ItemSpecialEffect flags) player._pHFrames = frames; player._pIFlags = flags; - StartPlrHit(player, 5, false); + // StartPlrHit compares damage (a 6 bit fixed point value) to character level to determine if the player shrugs off the hit. + // We don't initialise player so this comparison can't be relied on, instead we use forcehit to ensure the player enters hit mode + StartPlrHit(player, 0, true); int i = 1; for (; i < 100; i++) { @@ -109,7 +111,7 @@ static void AssertPlayer(Player &player) ASSERT_EQ(player._pDexterity, 30); ASSERT_EQ(player._pBaseVit, 20); ASSERT_EQ(player._pVitality, 20); - ASSERT_EQ(player._pLevel, 1); + ASSERT_EQ(player.getCharacterLevel(), 1); ASSERT_EQ(player._pStatPts, 0); ASSERT_EQ(player._pExperience, 0); ASSERT_EQ(player._pGold, 100); diff --git a/test/writehero_test.cpp b/test/writehero_test.cpp index 1fc7368a1..14bac6ab1 100644 --- a/test/writehero_test.cpp +++ b/test/writehero_test.cpp @@ -278,7 +278,7 @@ void AssertPlayer(Player &player) ASSERT_EQ(player._pDexterity, 281); ASSERT_EQ(player._pBaseVit, 80); ASSERT_EQ(player._pVitality, 90); - ASSERT_EQ(player._pLevel, 50); + ASSERT_EQ(player.getCharacterLevel(), 50); ASSERT_EQ(player._pStatPts, 0); ASSERT_EQ(player._pExperience, 1583495809); ASSERT_EQ(player._pGold, 0);