From e4fd4b4e6d3c7a869146ad5fdfd9c3197971e851 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 12 Mar 2026 23:59:38 -0700 Subject: [PATCH] feat: parse SMSG_SET_FLAT/PCT_SPELL_MODIFIER and apply talent modifiers to spell tooltips Implements SMSG_SET_FLAT_SPELL_MODIFIER and SMSG_SET_PCT_SPELL_MODIFIER (previously consumed silently). Parses per-group (uint8 groupIndex, uint8 SpellModOp, int32 value) tuples sent by the server after login and talent changes, and stores them in spellFlatMods_/spellPctMods_ maps keyed by (SpellModOp, groupIndex). Exposes getSpellFlatMod(op)/getSpellPctMod(op) accessors and a static applySpellMod() helper. Clears both maps on character login alongside spellCooldowns. Surfaces talent-modified mana cost and cast time in the spellbook tooltip via SpellModOp::Cost and SpellModOp::CastingTime lookups. --- include/game/game_handler.hpp | 83 +++++++++++++++++++++++++++++++++++ src/game/game_handler.cpp | 25 +++++++++-- src/ui/spellbook_screen.cpp | 20 ++++++--- 3 files changed, 120 insertions(+), 8 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 32af0860..08311cb2 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -1491,6 +1491,84 @@ public: }; const std::array& getPlayerRunes() const { return playerRunes_; } + // Talent-driven spell modifiers (SMSG_SET_FLAT_SPELL_MODIFIER / SMSG_SET_PCT_SPELL_MODIFIER) + // SpellModOp matches WotLK SpellModOp enum (server-side). + enum class SpellModOp : uint8_t { + Damage = 0, + Duration = 1, + Threat = 2, + Effect1 = 3, + Charges = 4, + Range = 5, + Radius = 6, + CritChance = 7, + AllEffects = 8, + NotLoseCastingTime = 9, + CastingTime = 10, + Cooldown = 11, + Effect2 = 12, + IgnoreArmor = 13, + Cost = 14, + CritDamageBonus = 15, + ResistMissChance = 16, + JumpTargets = 17, + ChanceOfSuccess = 18, + ActivationTime = 19, + Efficiency = 20, + MultipleValue = 21, + ResistDispelChance = 22, + Effect3 = 23, + BonusMultiplier = 24, + ProcPerMinute = 25, + ValueMultiplier = 26, + ResistPushback = 27, + MechanicDuration = 28, + StartCooldown = 29, + PeriodicBonus = 30, + AttackPower = 31, + }; + static constexpr int SPELL_MOD_OP_COUNT = 32; + + // Key: (SpellModOp, groupIndex) — value: accumulated flat or pct modifier + // pct values are stored in integer percent (e.g. -20 means -20% reduction). + struct SpellModKey { + SpellModOp op; + uint8_t group; + bool operator==(const SpellModKey& o) const { + return op == o.op && group == o.group; + } + }; + struct SpellModKeyHash { + std::size_t operator()(const SpellModKey& k) const { + return std::hash()( + (static_cast(static_cast(k.op)) << 8) | k.group); + } + }; + + // Returns the sum of all flat modifiers for a given op across all groups. + // (Callers that need per-group resolution can use getSpellFlatMods() directly.) + int32_t getSpellFlatMod(SpellModOp op) const { + int32_t total = 0; + for (const auto& [k, v] : spellFlatMods_) + if (k.op == op) total += v; + return total; + } + // Returns the sum of all pct modifiers for a given op across all groups (in %). + int32_t getSpellPctMod(SpellModOp op) const { + int32_t total = 0; + for (const auto& [k, v] : spellPctMods_) + if (k.op == op) total += v; + return total; + } + + // Convenience: apply flat+pct modifier to a base value. + // result = (base + flatMod) * (1.0 + pctMod/100.0), clamped to >= 0. + static int32_t applySpellMod(int32_t base, int32_t flat, int32_t pct) { + int64_t v = static_cast(base) + flat; + if (pct != 0) v = v + (v * pct + 50) / 100; // round half-up + return static_cast(v < 0 ? 0 : v); + } + struct FactionStandingInit { uint8_t flags = 0; int32_t standing = 0; @@ -3100,6 +3178,11 @@ private: // ---- WotLK Calendar: pending invite counter ---- uint32_t calendarPendingInvites_ = 0; ///< Unacknowledged calendar invites (SMSG_CALENDAR_SEND_NUM_PENDING) + + // ---- Spell modifiers (SMSG_SET_FLAT_SPELL_MODIFIER / SMSG_SET_PCT_SPELL_MODIFIER) ---- + // Keyed by (SpellModOp, groupIndex); cleared on logout/character change. + std::unordered_map spellFlatMods_; + std::unordered_map spellPctMods_; }; } // namespace game diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 7233b0ae..61daa687 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -3756,12 +3756,29 @@ void GameHandler::handlePacket(network::Packet& packet) { } case Opcode::SMSG_FEATURE_SYSTEM_STATUS: - case Opcode::SMSG_SET_FLAT_SPELL_MODIFIER: - case Opcode::SMSG_SET_PCT_SPELL_MODIFIER: - // Different formats than SMSG_SPELL_DELAYED — consume and ignore packet.setReadPos(packet.getSize()); break; + case Opcode::SMSG_SET_FLAT_SPELL_MODIFIER: + case Opcode::SMSG_SET_PCT_SPELL_MODIFIER: { + // WotLK format: one or more (uint8 groupIndex, uint8 modOp, int32 value) tuples + // Each tuple is 6 bytes; iterate until packet is consumed. + const bool isFlat = (*logicalOp == Opcode::SMSG_SET_FLAT_SPELL_MODIFIER); + auto& modMap = isFlat ? spellFlatMods_ : spellPctMods_; + while (packet.getSize() - packet.getReadPos() >= 6) { + uint8_t groupIndex = packet.readUInt8(); + uint8_t modOpRaw = packet.readUInt8(); + int32_t value = static_cast(packet.readUInt32()); + if (groupIndex > 5 || modOpRaw >= SPELL_MOD_OP_COUNT) continue; + SpellModKey key{ static_cast(modOpRaw), groupIndex }; + modMap[key] = value; + LOG_DEBUG(isFlat ? "SMSG_SET_FLAT_SPELL_MODIFIER" : "SMSG_SET_PCT_SPELL_MODIFIER", + ": group=", (int)groupIndex, " op=", (int)modOpRaw, " value=", value); + } + packet.setReadPos(packet.getSize()); + break; + } + case Opcode::SMSG_SPELL_DELAYED: { // WotLK: packed_guid (caster) + uint32 delayMs // TBC/Classic: uint64 (caster) + uint32 delayMs @@ -7930,6 +7947,8 @@ void GameHandler::selectCharacter(uint64_t characterGuid) { std::fill(std::begin(playerStats_), std::end(playerStats_), -1); knownSpells.clear(); spellCooldowns.clear(); + spellFlatMods_.clear(); + spellPctMods_.clear(); actionBar = {}; playerAuras.clear(); targetAuras.clear(); diff --git a/src/ui/spellbook_screen.cpp b/src/ui/spellbook_screen.cpp index 8c78ab7d..3d2ceeed 100644 --- a/src/ui/spellbook_screen.cpp +++ b/src/ui/spellbook_screen.cpp @@ -525,7 +525,7 @@ void SpellbookScreen::renderSpellTooltip(const SpellInfo* info, game::GameHandle // Resource cost + cast time on same row (WoW style) if (!info->isPassive()) { - // Left: resource cost + // Left: resource cost (with talent flat/pct modifier applied) char costBuf[64] = ""; if (info->manaCost > 0) { const char* powerName = "Mana"; @@ -535,16 +535,26 @@ void SpellbookScreen::renderSpellTooltip(const SpellInfo* info, game::GameHandle case 4: powerName = "Focus"; break; default: break; } - std::snprintf(costBuf, sizeof(costBuf), "%u %s", info->manaCost, powerName); + // Apply SMSG_SET_FLAT/PCT_SPELL_MODIFIER Cost modifier (SpellModOp::Cost = 14) + int32_t flatCost = gameHandler.getSpellFlatMod(game::GameHandler::SpellModOp::Cost); + int32_t pctCost = gameHandler.getSpellPctMod(game::GameHandler::SpellModOp::Cost); + uint32_t displayCost = static_cast( + game::GameHandler::applySpellMod(static_cast(info->manaCost), flatCost, pctCost)); + std::snprintf(costBuf, sizeof(costBuf), "%u %s", displayCost, powerName); } - // Right: cast time + // Right: cast time (with talent CastingTime modifier applied) char castBuf[32] = ""; if (info->castTimeMs == 0) { std::snprintf(castBuf, sizeof(castBuf), "Instant cast"); } else { - float secs = info->castTimeMs / 1000.0f; - std::snprintf(castBuf, sizeof(castBuf), "%.1f sec cast", secs); + // Apply SpellModOp::CastingTime (10) modifiers + int32_t flatCT = gameHandler.getSpellFlatMod(game::GameHandler::SpellModOp::CastingTime); + int32_t pctCT = gameHandler.getSpellPctMod(game::GameHandler::SpellModOp::CastingTime); + int32_t modCT = game::GameHandler::applySpellMod( + static_cast(info->castTimeMs), flatCT, pctCT); + float secs = static_cast(modCT) / 1000.0f; + std::snprintf(castBuf, sizeof(castBuf), "%.1f sec cast", secs > 0.0f ? secs : 0.0f); } if (costBuf[0] || castBuf[0]) {