mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-07-31 01:41:14 -04:00
159 lines
5.9 KiB
C++
159 lines
5.9 KiB
C++
#pragma once
|
|
|
|
// Pure spell-classification rules, kept free of GameHandler so they can be tested
|
|
// directly. Every decision here is driven by Spell.dbc / SpellRange.dbc data rather
|
|
// than by hardcoded spell ids: classifying by school instead once cost us Battle Shout
|
|
// (a physical-school self-buff read as melee) and every Hunter shot past 8 yards.
|
|
|
|
#include <cstdint>
|
|
#include <cstddef>
|
|
#include <functional>
|
|
#include <string>
|
|
#include <unordered_set>
|
|
|
|
namespace wowee {
|
|
namespace game {
|
|
namespace spellclass {
|
|
|
|
/// SpellRange calls melee "Combat Range", and every melee ability resolves to it.
|
|
inline constexpr float kCombatRangeYards = 5.0f;
|
|
|
|
/// Returned when SpellRange.dbc was unavailable — nothing may be inferred from it.
|
|
inline constexpr float kUnknownRange = -1.0f;
|
|
|
|
/// "Self Only" range: the spell cannot reach another unit, so it lands on the caster
|
|
/// no matter what is targeted (shouts, self-buffs, hearthstone).
|
|
inline bool isSelfCastRange(float maxRange) {
|
|
return maxRange == 0.0f;
|
|
}
|
|
|
|
/// A melee ability, i.e. one cast at Combat Range. Ranged physical abilities such as
|
|
/// Steady Shot or Taunt carry 30-35 yards and are deliberately excluded. An unknown
|
|
/// range is not melee: the server should arbitrate rather than the client blocking a
|
|
/// legitimate cast.
|
|
inline bool isMeleeRange(float maxRange) {
|
|
return maxRange > 0.0f && maxRange <= kCombatRangeYards;
|
|
}
|
|
|
|
/// Legacy client IDs for the three repeating ranged weapon attacks.
|
|
///
|
|
/// Their Spell.dbc rows carry a dummy one-point rage cost even for classes that do
|
|
/// not use rage. The server drives their real weapon/ammunition readiness, so UI
|
|
/// resource checks must not treat that placeholder as a cast cost.
|
|
inline bool isRangedWeaponAutoAttack(uint32_t spellId) {
|
|
return spellId == 75 || // Auto Shot
|
|
spellId == 5019 || // Shoot (wand)
|
|
spellId == 2764; // Throw
|
|
}
|
|
|
|
/// The learnable Fishing casts across Classic, TBC, and WotLK.
|
|
///
|
|
/// Fishing is unusual: it has no explicit unit target. The server chooses a point in
|
|
/// a narrow cone in front of the caster and validates water depth there. Keeping this
|
|
/// classification explicit prevents a selected creature from leaking into the cast
|
|
/// packet and lets the client select the pole-specific animation sequence.
|
|
inline bool isFishingCast(uint32_t spellId) {
|
|
return spellId == 7620 || // Fishing (Apprentice)
|
|
spellId == 7731 || // Fishing (Journeyman)
|
|
spellId == 7732 || // Fishing (Expert)
|
|
spellId == 18248 || // Fishing (Artisan)
|
|
spellId == 33095 || // Fishing (Master)
|
|
spellId == 51294; // Fishing (Grand Master)
|
|
}
|
|
|
|
/// Restoration food/water and instant potions use the dedicated consumption
|
|
/// animation. Match canonical Food/Drink names and actual potion-use suffixes
|
|
/// without classifying recipe spells that create potions.
|
|
enum class RestChannelKind {
|
|
NONE,
|
|
FOOD,
|
|
DRINK,
|
|
POTION,
|
|
ALCOHOL
|
|
};
|
|
|
|
inline RestChannelKind classifyRestChannel(const std::string& spellName) {
|
|
if (spellName == "Food") return RestChannelKind::FOOD;
|
|
if (spellName == "Drink") return RestChannelKind::DRINK;
|
|
constexpr const char* potionSuffix = "Potion";
|
|
if (spellName.size() >= 6 &&
|
|
spellName.compare(spellName.size() - 6, 6, potionSuffix) == 0 &&
|
|
spellName.rfind("Create ", 0) != 0) {
|
|
return RestChannelKind::POTION;
|
|
}
|
|
return RestChannelKind::NONE;
|
|
}
|
|
|
|
inline bool hasInebriateEffect(const uint32_t* effectIds, size_t count) {
|
|
constexpr uint32_t kSpellEffectInebriate = 100;
|
|
if (!effectIds) return false;
|
|
for (size_t i = 0; i < count; ++i) {
|
|
if (effectIds[i] == kSpellEffectInebriate) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// Spell.dbc stores the rank as a display string ("Rank 3"). Rankless spells sort as 0.
|
|
inline int rankValue(const std::string& rank) {
|
|
int value = 0;
|
|
bool sawDigit = false;
|
|
for (char c : rank) {
|
|
if (c >= '0' && c <= '9') {
|
|
sawDigit = true;
|
|
value = value * 10 + (c - '0');
|
|
} else if (sawDigit) {
|
|
break; // stop at the first non-digit after the number
|
|
}
|
|
}
|
|
return value;
|
|
}
|
|
|
|
/// Name and rank as they appear in Spell.dbc.
|
|
struct SpellRankInfo {
|
|
std::string name;
|
|
std::string rank;
|
|
};
|
|
|
|
/// Maps a superseded spell rank onto the highest rank the player actually knows.
|
|
///
|
|
/// Action bars restored from the server can still hold a rank that a higher rank has
|
|
/// since superseded. The server drops casts of superseded ranks without sending any
|
|
/// error at all, so the client has to correct them itself.
|
|
///
|
|
/// Returns spellId unchanged when it is already known, or when no known spell shares
|
|
/// its name (item, gather and vehicle spells are absent from the known list too, and
|
|
/// must be left alone).
|
|
///
|
|
/// `lookup` resolves a spell id to its Spell.dbc name/rank, or null if unknown.
|
|
inline uint32_t resolveHighestKnownRank(
|
|
uint32_t spellId,
|
|
const std::unordered_set<uint32_t>& knownSpells,
|
|
const std::function<const SpellRankInfo*(uint32_t)>& lookup) {
|
|
if (spellId == 0 || knownSpells.count(spellId) > 0) return spellId;
|
|
|
|
const SpellRankInfo* info = lookup(spellId);
|
|
if (!info || info->name.empty()) return spellId;
|
|
|
|
// Keep the requested name by value. Some DBC adapters reuse one scratch object
|
|
// for every lookup; retaining its pointer here lets the first candidate overwrite
|
|
// the requested spell and makes unrelated known spells look like matching ranks.
|
|
const std::string requestedName = info->name;
|
|
|
|
uint32_t best = 0;
|
|
int bestRank = -1;
|
|
for (uint32_t known : knownSpells) {
|
|
const SpellRankInfo* candidate = lookup(known);
|
|
if (!candidate || candidate->name != requestedName) continue;
|
|
const int rank = rankValue(candidate->rank);
|
|
if (rank > bestRank) {
|
|
bestRank = rank;
|
|
best = known;
|
|
}
|
|
}
|
|
return best != 0 ? best : spellId;
|
|
}
|
|
|
|
} // namespace spellclass
|
|
} // namespace game
|
|
} // namespace wowee
|