diff --git a/src/server/game/Entities/Item/Item.cpp b/src/server/game/Entities/Item/Item.cpp index 9aea0ea..529e6e7 100644 --- a/src/server/game/Entities/Item/Item.cpp +++ b/src/server/game/Entities/Item/Item.cpp @@ -898,7 +898,31 @@ bool Item::IsFitToSpellRequirements(SpellInfo const* spellInfo) const if (spellInfo->EquippedItemSubClassMask != 0) // 0 == any subclass { - if ((spellInfo->EquippedItemSubClassMask & (1 << proto->SubClass)) == 0) + uint32 subclassMask = spellInfo->EquippedItemSubClassMask & (1 << proto->SubClass); + // Two-handers use *_2 subclasses; many strike spells only set one-hand bits (main or off hand). + if (!subclassMask && proto->Class == ITEM_CLASS_WEAPON && proto->InventoryType == INVTYPE_2HWEAPON) + { + if (Player const* player = GetOwner() ? GetOwner()->ToPlayer() : nullptr) + { + if (player && IsEquipped() && GetBagSlot() == INVENTORY_SLOT_BAG_0 && + (GetSlot() == EQUIPMENT_SLOT_OFFHAND || GetSlot() == EQUIPMENT_SLOT_MAINHAND)) + { + int32 pairedSubclass = -1; + switch (proto->SubClass) + { + case ITEM_SUBCLASS_WEAPON_AXE2: pairedSubclass = ITEM_SUBCLASS_WEAPON_AXE; break; + case ITEM_SUBCLASS_WEAPON_MACE2: pairedSubclass = ITEM_SUBCLASS_WEAPON_MACE; break; + case ITEM_SUBCLASS_WEAPON_SWORD2: pairedSubclass = ITEM_SUBCLASS_WEAPON_SWORD; break; + case ITEM_SUBCLASS_WEAPON_EXOTIC2: pairedSubclass = ITEM_SUBCLASS_WEAPON_EXOTIC; break; + default: break; + } + if (pairedSubclass >= 0) + subclassMask = spellInfo->EquippedItemSubClassMask & (1 << pairedSubclass); + } + } + } + + if (!subclassMask) return false; // subclass not present in mask } } @@ -910,6 +934,25 @@ bool Item::IsFitToSpellRequirements(SpellInfo const* spellInfo) const (spellInfo->EquippedItemInventoryTypeMask & (1 << INVTYPE_WEAPONMAINHAND) || spellInfo->EquippedItemInventoryTypeMask & (1 << INVTYPE_WEAPONOFFHAND))) return true; + // 2H in off-hand or main-hand: many spells only list 1H / generic inventory types. + if (proto->InventoryType == INVTYPE_2HWEAPON) + { + if (Player const* player = GetOwner() ? GetOwner()->ToPlayer() : nullptr) + { + if (player && IsEquipped() && GetBagSlot() == INVENTORY_SLOT_BAG_0) + { + if (GetSlot() == EQUIPMENT_SLOT_OFFHAND && + ((spellInfo->EquippedItemInventoryTypeMask & (1 << INVTYPE_WEAPONOFFHAND)) || + (spellInfo->EquippedItemInventoryTypeMask & (1 << INVTYPE_WEAPON)))) + return true; + if (GetSlot() == EQUIPMENT_SLOT_MAINHAND && + ((spellInfo->EquippedItemInventoryTypeMask & (1 << INVTYPE_WEAPONMAINHAND)) || + (spellInfo->EquippedItemInventoryTypeMask & (1 << INVTYPE_WEAPON)) || + (spellInfo->EquippedItemInventoryTypeMask & (1 << INVTYPE_2HWEAPON)))) + return true; + } + } + } else if ((spellInfo->EquippedItemInventoryTypeMask & (1 << proto->InventoryType)) == 0) return false; // inventory type not present in mask } diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index 5ed8ca2..5676e10 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -3088,6 +3088,12 @@ bool Player::CheckSkillLearnedBySpell(uint32 spellId) if (!sWorld->getBoolConfig(CONFIG_VALIDATE_SKILL_LEARNED_BY_SPELLS)) return true; + // Titan Grip (e.g. talent 46917) ties to warrior-only skill lines in DBC. Classless / cross-class + // characters must keep the spell; otherwise it is deleted on every login and 2H off-hand unequips. + if (SpellInfo const* si = sSpellMgr->GetSpellInfo(spellId)) + if (si->HasEffect(SPELL_EFFECT_TITAN_GRIP)) + return true; + SkillLineAbilityMapBounds skill_bounds = sSpellMgr->GetSkillLineAbilityMapBounds(spellId); uint32 errorSkill = 0; for (SkillLineAbilityMap::const_iterator sla = skill_bounds.first; sla != skill_bounds.second; ++sla) @@ -3773,8 +3779,8 @@ bool Player::resetTalents(bool noResetCost) _removeTalent(itr, GetActiveSpecMask()); } - // xinef: remove titan grip if player had it set - if (m_canTitanGrip) + // xinef: remove titan grip on warrior talent reset (other classes may use custom Titan Grip) + if (IsClass(CLASS_WARRIOR, CLASS_CONTEXT_ABILITY) && m_canTitanGrip) SetCanTitanGrip(false); // xinef: remove dual wield if player does not have dual wield spell (shamans) if (!HasSpell(674) && CanDualWield()) @@ -15409,8 +15415,8 @@ void Player::ActivateSpec(uint8 spec) SetPower(POWER_MANA, 0); // Mana must be 0 even if it isn't the active power type. SetPower(pw, 0); - // xinef: remove titan grip if player had it set and does not have appropriate talent - if (!HasTalent(46917, GetActiveSpec()) && m_canTitanGrip) + // xinef: remove titan grip if warrior had it set and does not have appropriate talent (other classes may use Titan Grip from custom spells) + if (IsClass(CLASS_WARRIOR, CLASS_CONTEXT_ABILITY) && !HasTalent(46917, GetActiveSpec()) && m_canTitanGrip) SetCanTitanGrip(false); // xinef: remove dual wield if player does not have dual wield spell (shamans) if (!HasSpell(674) && CanDualWield()) diff --git a/src/server/game/Entities/Player/PlayerStorage.cpp b/src/server/game/Entities/Player/PlayerStorage.cpp index 51ff089..608a2e0 100644 --- a/src/server/game/Entities/Player/PlayerStorage.cpp +++ b/src/server/game/Entities/Player/PlayerStorage.cpp @@ -5525,6 +5525,21 @@ bool Player::LoadFromDB(ObjectGuid playerGuid, CharacterDatabaseQueryHolder cons // xinef: load mails before inventory, so problematic items can be added to already loaded mails _LoadMail(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_MAILS), holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_MAIL_ITEMS)); + // m_canTitanGrip is normally set when SPELL_EFFECT_TITAN_GRIP runs. After spellbook load, ensure the + // flag matches any known TG spell so 2H off-hand items validate before _LoadInventory (classless DK, etc.). + for (auto const& spellItr : m_spells) + { + if (!spellItr.second->Active || spellItr.second->State == PLAYERSPELL_REMOVED) + continue; + if (SpellInfo const* si = sSpellMgr->GetSpellInfo(spellItr.first)) + if (si->HasEffect(SPELL_EFFECT_TITAN_GRIP)) + { + SetCanTitanGrip(true); + UpdateTitansGrip(); + break; + } + } + _LoadInventory(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_INVENTORY), time_diff); // update items with duration and realtime diff --git a/src/server/game/Spells/Spell.cpp b/src/server/game/Spells/Spell.cpp index 544997f..2084e1d 100644 --- a/src/server/game/Spells/Spell.cpp +++ b/src/server/game/Spells/Spell.cpp @@ -61,6 +61,34 @@ #include "IVMapMgr.h" #include "VMapMgr2.h" +namespace +{ +// Threat of Thassarian off-hand strikes (all ranks). DBC often omits SPELL_ATTR3_REQUIRES_OFF_HAND_WEAPON, +// so m_attackType would stay BASE_ATTACK and item/damage use the wrong hand (breaks dual 2H / Titan Grip). +bool IsDKThassarianOffHandStrikeSpell(SpellInfo const* spellInfo) +{ + if (!spellInfo || spellInfo->SpellFamilyName != SPELLFAMILY_DEATHKNIGHT || spellInfo->DmgClass != SPELL_DAMAGE_CLASS_MELEE) + return false; + + SpellInfo const* first = spellInfo->GetFirstRankSpell(); + if (!first) + return false; + + switch (first->Id) + { + case 66198: // Obliterate + case 66196: // Frost Strike + case 66216: // Plague Strike + case 66188: // Death Strike + case 66217: // Rune Strike + case 66215: // Blood Strike + return true; + default: + return false; + } +} +} // namespace + extern pEffect SpellEffects[TOTAL_SPELL_EFFECTS]; SpellDestination::SpellDestination() @@ -591,7 +619,7 @@ Spell::Spell(Unit* caster, SpellInfo const* info, TriggerCastFlags triggerFlags, switch (m_spellInfo->DmgClass) { case SPELL_DAMAGE_CLASS_MELEE: - if (m_spellInfo->HasAttribute(SPELL_ATTR3_REQUIRES_OFF_HAND_WEAPON)) + if (m_spellInfo->HasAttribute(SPELL_ATTR3_REQUIRES_OFF_HAND_WEAPON) || IsDKThassarianOffHandStrikeSpell(m_spellInfo)) m_attackType = OFF_ATTACK; else m_attackType = BASE_ATTACK;