feat: color cast bars green/red by spell interruptibility from Spell.dbc

Load AttributesEx from Spell.dbc for all expansions (Classic/TBC/WotLK/
Turtle). Check SPELL_ATTR_EX_NOT_INTERRUPTIBLE (bit 4 = 0x10) to classify
each cast as interruptible or not when SMSG_SPELL_START arrives.

Target frame and nameplate cast bars now use:
- Green: spell can be interrupted by Kick/Counterspell/Pummel etc.
- Red: spell is immune to interrupt (boss abilities, instant-cast effects)
Both colors pulse faster at >80% completion to signal the closing window.

Adds GameHandler::isSpellInterruptible() and UnitCastState::interruptible.
This commit is contained in:
Kelsi 2026-03-17 19:43:19 -07:00
parent b8712f380d
commit 279b4de09a
7 changed files with 55 additions and 17 deletions

View file

@ -18279,10 +18279,11 @@ void GameHandler::handleSpellStart(network::Packet& packet) {
// Track cast bar for any non-player caster (target frame + boss frames)
if (data.casterUnit != playerGuid && data.castTime > 0) {
auto& s = unitCastStates_[data.casterUnit];
s.casting = true;
s.spellId = data.spellId;
s.timeTotal = data.castTime / 1000.0f;
s.timeRemaining = s.timeTotal;
s.casting = true;
s.spellId = data.spellId;
s.timeTotal = data.castTime / 1000.0f;
s.timeRemaining = s.timeTotal;
s.interruptible = isSpellInterruptible(data.spellId);
// Trigger cast animation on the casting unit
if (spellCastAnimCallback_) {
spellCastAnimCallback_(data.casterUnit, true, false);
@ -21320,6 +21321,14 @@ void GameHandler::loadSpellNameCache() {
if (f != 0xFFFFFFFF && f < dbc->getFieldCount()) { dispelField = f; hasDispelField = true; }
}
// AttributesEx field (bit 4 = SPELL_ATTR_EX_NOT_INTERRUPTIBLE)
uint32_t attrExField = 0xFFFFFFFF;
bool hasAttrExField = false;
if (spellL) {
uint32_t f = spellL->field("AttributesEx");
if (f != 0xFFFFFFFF && f < dbc->getFieldCount()) { attrExField = f; hasAttrExField = true; }
}
// Tooltip/description field
uint32_t tooltipField = 0xFFFFFFFF;
if (spellL) {
@ -21334,7 +21343,7 @@ void GameHandler::loadSpellNameCache() {
std::string name = dbc->getString(i, spellL ? (*spellL)["Name"] : 136);
std::string rank = dbc->getString(i, spellL ? (*spellL)["Rank"] : 153);
if (!name.empty()) {
SpellNameEntry entry{std::move(name), std::move(rank), {}, 0, 0};
SpellNameEntry entry{std::move(name), std::move(rank), {}, 0, 0, 0};
if (tooltipField != 0xFFFFFFFF) {
entry.description = dbc->getString(i, tooltipField);
}
@ -21349,6 +21358,9 @@ void GameHandler::loadSpellNameCache() {
if (hasDispelField) {
entry.dispelType = static_cast<uint8_t>(dbc->getUInt32(i, dispelField));
}
if (hasAttrExField) {
entry.attrEx = dbc->getUInt32(i, attrExField);
}
spellNameCache_[id] = std::move(entry);
}
}
@ -21554,6 +21566,15 @@ uint8_t GameHandler::getSpellDispelType(uint32_t spellId) const {
return (it != spellNameCache_.end()) ? it->second.dispelType : 0;
}
bool GameHandler::isSpellInterruptible(uint32_t spellId) const {
if (spellId == 0) return true;
const_cast<GameHandler*>(this)->loadSpellNameCache();
auto it = spellNameCache_.find(spellId);
if (it == spellNameCache_.end()) return true; // assume interruptible if unknown
// SPELL_ATTR_EX_NOT_INTERRUPTIBLE = bit 4 of AttributesEx (0x00000010)
return (it->second.attrEx & 0x00000010u) == 0;
}
const std::string& GameHandler::getSkillLineName(uint32_t spellId) const {
auto slIt = spellToSkillLine_.find(spellId);
if (slIt == spellToSkillLine_.end()) return EMPTY_STRING;