Skip to content

Commit 3003b11

Browse files
committed
Add C_Spell.GetSpellPowerCost(spellIdentifier)
Returns a SpellPowerCostInfo[] (one entry in vanilla — spells have a single power cost), or nil for an unknown / no-cost spell. The `cost` is the player-effective value via the engine's own cost helper FUN_006e31b0 (base + level + ManaCostPercent-of-resource + descriptor power-cost mods + the cost SpellMod, op 0xE) — so it matches what casting actually deducts, talents included. Verified in-game: Blizzard (10187) on a Mage with 3/3 Frost Channeling returns cost 1190 = 1400 base x 0.85 (15% reduction). Keeps C_Spell.GetSpellInfo's `cost` field as the base ManaCost (intrinsic spell data); GetSpellPowerCost is the player-modified counterpart — same base/modified split as GetSpellRadius. Calling the full engine helper beats replicating op 14 via Spell::Mod, since the helper also folds in the descriptor power-cost modifiers. Fields: type (Enum.PowerType), name (power token), cost, minCost (==cost, no optional cost in vanilla), costPercent, costPerSec 0, requiredAuraID 0, hasRequiredAura false. Docs: API.md section + README Spell row.
1 parent bf92501 commit 3003b11

4 files changed

Lines changed: 158 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
4747
| [NamePlate](docs/API.md#nameplate) | `C_NamePlate.GetNamePlateForGUID`, `C_NamePlate.GetNamePlateForUnit`, `C_NamePlate.GetNamePlateGUIDs`, `C_NamePlate.GetNamePlates` |
4848
| [NameCache](docs/API.md#namecache) | `C_CreatureInfo.GetCreatureID`, `C_PlayerCache.GetPlayerInfoByName`, `C_PlayerCache.IsEnabled`, `C_PlayerCache.IsScanEnabled`, `C_PlayerCache.RememberPlayer`, `C_PlayerCache.SetEnabled`, `C_PlayerCache.SetScanEnabled`, `C_PlayerInfo.GUIDIsCreature`, `C_PlayerInfo.GUIDIsGameObject`, `C_PlayerInfo.GUIDIsPet`, `C_PlayerInfo.GUIDIsPlayer`, `GetPlayerInfoByGUID`, `UnitNameFromGUID` |
4949
| [Quest](docs/API.md#quest) | `C_QuestLog.GetQuestDetails`, `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` |
50-
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.SpellHasRange`, `CancelSpellByName`, `CastingInfo`, `CastSpellNoToggle`, `ChannelInfo`, `GetCraftSpellID`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange`, `UnitCastingInfo`, `UnitChannelInfo` |
50+
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.SpellHasRange`, `CancelSpellByName`, `CastingInfo`, `CastSpellNoToggle`, `ChannelInfo`, `GetCraftSpellID`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange`, `UnitCastingInfo`, `UnitChannelInfo` |
5151
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `C_SpellBook.IsAutoAttackSpellBookItem`, `C_SpellBook.IsRangedAutoAttackSpellBookItem`, `FindSpellBookSlotByID` |
5252
| [State](docs/API.md#state) | `CancelShapeshiftForm`, `Dismount`, `GetMirrorTimerInfo`, `GetMirrorTimerProgress`, `GetShapeshiftFormID`, `IsAssistingRitual`, `IsFalling`, `IsInGroup`, `IsInRaid`, `IsLoggedIn`, `IsMounted`, `IsStealthed`, `IsSwimming` |
5353
| [Table](docs/API.md#table) | `table.wipe` |

docs/API.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ build instructions.
281281
- [`C_Spell.GetSpellDescription(spellID)`](#c_spellgetspelldescriptionspellid)
282282
- [`C_Spell.GetSpellMechanicByID(spellID)`](#c_spellgetspellmechanicbyidspellid)
283283
- [`C_Spell.GetSpellRadius(spellID)` / `GetSpellRadius(slot, bookType)`](#c_spellgetspellradiusspellid--getspellradiusslot-booktype)
284+
- [`C_Spell.GetSpellPowerCost(spellIdentifier)`](#c_spellgetspellpowercostspellidentifier)
284285
- [`C_Spell.GetSpellReagents(spellID)`](#c_spellgetspellreagentsspellid)
285286
- [`C_Spell.GetSpellSubtext(spellIdentifier)`](#c_spellgetspellsubtextspellidentifier)
286287
- [`IsPassiveSpell(spellID)` / `IsPassiveSpell(slot, bookType)`](#ispassivespellspellid--ispassivespellslot-booktype)
@@ -6928,6 +6929,43 @@ across the three effects is returned.
69286929
> another caster's talents). All DBCs / mod tables are resident from
69296930
> boot, so the call is synchronous with no caching.
69306931
6932+
### `C_Spell.GetSpellPowerCost(spellIdentifier)`
6933+
6934+
Returns an array of `SpellPowerCostInfo` tables, or `nil` if the spell
6935+
isn't found or has no resource cost. Vanilla spells have exactly one
6936+
power cost, so the array holds at most one entry.
6937+
6938+
```lua
6939+
C_Spell.GetSpellPowerCost(10187) -- Blizzard, Mage with 3/3 Frost Channeling
6940+
-- { [1] = { type = 0, name = "MANA", cost = 1190, minCost = 1190,
6941+
-- costPercent = 0, costPerSec = 0, requiredAuraID = 0,
6942+
-- hasRequiredAura = false } }
6943+
-- base cost 1400, 15% reduction applied -> 1190
6944+
```
6945+
6946+
| Field | Meaning |
6947+
|-------|---------|
6948+
| `type` | `Enum.PowerType``0` mana, `1` rage, `2` focus, `3` energy, `4` happiness (the spell's `PowerType`) |
6949+
| `name` | power token (`"MANA"`, `"RAGE"`, …) |
6950+
| `cost` | **effective** cost for the local player |
6951+
| `minCost` | `== cost` (vanilla has no optional-cost component) |
6952+
| `costPercent` | the spell's `%`-of-base-resource cost, or `0` for a flat cost |
6953+
| `costPerSec` | `0` (vanilla doesn't expose a per-second channel cost in these terms) |
6954+
| `requiredAuraID` | `0` (no form/aura-conditional costs in 1.12) |
6955+
| `hasRequiredAura` | `false` |
6956+
6957+
`cost` is the value the engine actually charges — base + level scaling +
6958+
`ManaCostPercent`-of-resource + descriptor power-cost mods + the cost
6959+
SpellMod (talents like Frost Channeling). It comes from the engine's own
6960+
cost helper (`FUN_006e31b0`), so it stays in lockstep with what casting
6961+
the spell deducts.
6962+
6963+
> **Effective vs. base.** This returns the player-modified cost.
6964+
> [`C_Spell.GetSpellInfo`](#c_spellgetspellinfospellid)'s `cost` field is
6965+
> the **base** `ManaCost` (intrinsic spell data, caster-independent) —
6966+
> same base/modified split as
6967+
> [`GetSpellRadius`](#c_spellgetspellradiusspellid--getspellradiusslot-booktype).
6968+
69316969
### `C_Spell.GetSpellReagents(spellID)`
69326970

69336971
Returns the spell's reagent list as an array of

src/Offsets.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,16 @@ enum Offsets {
639639
// instant / no-cast-time spells.
640640
FUN_GET_CAST_TIME = 0x006E3340,
641641

642+
// Effective power cost (mana/rage/energy/…) for a spell:
643+
// `__fastcall uint32_t(int spellID, int unit /*0 = local player*/)`.
644+
// Computes ManaCost base + per-level + ManaCostPercent-of-resource,
645+
// applies the descriptor power-cost modifiers, then the cost
646+
// SpellMod (op 0xE / 14). 0xFFFFFFFF on invalid input / no player.
647+
// Read by C_Spell.GetSpellPowerCost (the full helper, like the cast
648+
// time one — beats replicating op 14 via Spell::Mod since it also
649+
// folds in the descriptor cost mods).
650+
FUN_GET_SPELL_COST = 0x006E31B0,
651+
642652
// Spell.dbc `m_durationIndex` field — pointer into SpellDuration.dbc.
643653
// Verified via `FUN_004E44B0` (`0x004e44b0`) and `FUN_006EA000`
644654
// (`0x006ea000`), both of which read `[spellRec + 0x78]` and use

src/spell/PowerCost.cpp

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// This file is part of ClassicAPI.
2+
//
3+
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
4+
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
5+
// version 3 of the License, or (at your option) any later version.
6+
//
7+
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
8+
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
9+
// PURPOSE. See the GNU Lesser General Public License for more details.
10+
//
11+
// You should have received a copy of the GNU Lesser General Public License along with
12+
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
13+
14+
// `C_Spell.GetSpellPowerCost(spellIdentifier)` -> SpellPowerCostInfo[]
15+
//
16+
// Returns an array of power-cost tables, or nil if the spell isn't found
17+
// or has no resource cost. Vanilla spells have exactly one power cost, so
18+
// the array always holds at most one entry.
19+
//
20+
// `cost` is the **effective** cost for the local player — base + level
21+
// scaling + ManaCostPercent-of-resource + the descriptor power-cost
22+
// modifiers + the cost SpellMod (op 14) — via the engine's own cost
23+
// helper `FUN_006e31b0` (the same value the engine charges). So talents
24+
// like reduced-cost spells are reflected. Per-entry fields:
25+
//
26+
// type Enum.PowerType (0 mana, 1 rage, 2 focus, 3 energy,
27+
// 4 happiness) — the spell's PowerType (+0x7C)
28+
// name power token ("MANA", "RAGE", …)
29+
// cost effective cost (see above)
30+
// minCost == cost; vanilla has no "optional" cost component
31+
// costPercent ManaCostPercent (+0x270) — the spell's %-of-base-
32+
// resource cost, or 0 for a flat cost
33+
// costPerSec 0 — vanilla doesn't expose a per-second channel cost
34+
// in these terms
35+
// requiredAuraID 0 — vanilla has no form/aura-conditional costs here
36+
// hasRequiredAura false
37+
38+
#include "Game.h"
39+
#include "Offsets.h"
40+
#include "spell/Arg.h"
41+
#include "spell/Lookup.h"
42+
43+
#include <cstdint>
44+
45+
namespace Spell::PowerCost {
46+
47+
namespace {
48+
49+
using GetSpellCost_t = uint32_t(__fastcall *)(int spellID, int unit);
50+
51+
constexpr int OFF_POWER_TYPE = 0x7C; // int PowerType
52+
constexpr int OFF_MANA_COST_PERCENT = 0x270; // int, % of base resource (0 = flat)
53+
54+
// Power token by PowerType, matching the engine's POWER_TYPE strings.
55+
const char *PowerToken(int powerType) {
56+
switch (powerType) {
57+
case 0: return "MANA";
58+
case 1: return "RAGE";
59+
case 2: return "FOCUS";
60+
case 3: return "ENERGY";
61+
case 4: return "HAPPINESS";
62+
default: return ""; // e.g. health-cost spells (PowerType -2)
63+
}
64+
}
65+
66+
uint32_t EffectiveCost(int spellID) {
67+
return reinterpret_cast<GetSpellCost_t>(Offsets::FUN_GET_SPELL_COST)(spellID, 0);
68+
}
69+
70+
int __fastcall Script_GetSpellPowerCost(void *L) {
71+
const int spellID = Spell::Arg::ResolveSpellID(L, 1);
72+
const uint8_t *rec = Spell::Lookup::RecordForID(spellID);
73+
if (rec == nullptr)
74+
return 0; // nil — spell not found
75+
76+
const uint32_t cost = EffectiveCost(spellID);
77+
if (cost == 0xFFFFFFFF)
78+
return 0; // engine error (no player context, etc.)
79+
80+
const int powerType = *reinterpret_cast<const int *>(rec + OFF_POWER_TYPE);
81+
const int costPercent = *reinterpret_cast<const int *>(rec + OFF_MANA_COST_PERCENT);
82+
if (cost == 0 && costPercent == 0)
83+
return 0; // nil — no resource cost
84+
85+
// SpellPowerCostInfo[] — a one-element array: outer[1] = entry.
86+
Game::Lua::NewTable(L); // outer
87+
Game::Lua::PushNumber(L, 1.0); // key 1
88+
Game::Lua::NewTable(L); // entry (top)
89+
Game::Lua::SetFieldNumber(L, "type", static_cast<double>(powerType));
90+
Game::Lua::SetFieldString(L, "name", PowerToken(powerType));
91+
Game::Lua::SetFieldNumber(L, "cost", static_cast<double>(cost));
92+
Game::Lua::SetFieldNumber(L, "minCost", static_cast<double>(cost));
93+
Game::Lua::SetFieldNumber(L, "costPercent", static_cast<double>(costPercent));
94+
Game::Lua::SetFieldNumber(L, "costPerSec", 0);
95+
Game::Lua::SetFieldNumber(L, "requiredAuraID", 0);
96+
Game::Lua::SetFieldBool(L, "hasRequiredAura", false);
97+
Game::Lua::SetTable(L, -3); // outer[1] = entry
98+
return 1;
99+
}
100+
101+
} // namespace
102+
103+
static void RegisterLuaFunctions() {
104+
Game::Lua::RegisterTableFunction("C_Spell", "GetSpellPowerCost", &Script_GetSpellPowerCost);
105+
}
106+
107+
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
108+
109+
} // namespace Spell::PowerCost

0 commit comments

Comments
 (0)