mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-12 12:57:37 +00:00
Compare commits
150
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ae69fd144 | ||
|
|
454be4ce49 | ||
|
|
717d75cd33 | ||
|
|
d4af7c4c6a | ||
|
|
1b267039a6 | ||
|
|
930f969cd6 | ||
|
|
d2e52ea6dd | ||
|
|
6422401ca7 | ||
|
|
b5e97d7d98 | ||
|
|
ca61a831f8 | ||
|
|
e39b87ab04 | ||
|
|
fb17670e37 | ||
|
|
0963a4c38f | ||
|
|
8c4a9be0dd | ||
|
|
a16b99073a | ||
|
|
d7103de564 | ||
|
|
c00eb24a8d | ||
|
|
b65c9e0319 | ||
|
|
de033896b1 | ||
|
|
dc8d4caf12 | ||
|
|
20069f3815 | ||
|
|
f2fa112f0a | ||
|
|
fca967c827 | ||
|
|
24404b5a23 | ||
|
|
a106d86969 | ||
|
|
fac3be3a0b | ||
|
|
b49c107778 | ||
|
|
53b7f79440 | ||
|
|
7b21453469 | ||
|
|
ebfd9d034d | ||
|
|
d825bcbdda | ||
|
|
6b36bb10e6 | ||
|
|
6628dcca19 | ||
|
|
55d94c80d6 | ||
|
|
c0b403ba05 | ||
|
|
f6b98bc3ec | ||
|
|
8f4f574174 | ||
|
|
43a989dd56 | ||
|
|
0caddb8fe6 | ||
|
|
8e603a7a0b | ||
|
|
4726fd7c85 | ||
|
|
3e0c40184b | ||
|
|
b3e8d92b03 | ||
|
|
0a3f8324ea | ||
|
|
6f3a8ffc1d | ||
|
|
3862408e1a | ||
|
|
12a81f7f89 | ||
|
|
05fa5845b7 | ||
|
|
6ed0edc947 | ||
|
|
1645e6b43c | ||
|
|
efcdc54a50 | ||
|
|
6a24271cd1 | ||
|
|
a7ce7555f0 | ||
|
|
04790cb6c1 | ||
|
|
e7ecae56da | ||
|
|
65c6b0b271 | ||
|
|
5f94fb98bd | ||
|
|
426f6b6ec9 | ||
|
|
3a4d291748 | ||
|
|
1cdd46958f | ||
|
|
72f0e5d98f | ||
|
|
ceb6aaf46b | ||
|
|
84c936faa4 | ||
|
|
957d66af49 | ||
|
|
3849b7513b | ||
|
|
1cdd443e53 | ||
|
|
ada33825ad | ||
|
|
dd584703a2 | ||
|
|
5911298acd | ||
|
|
a6fa06faa0 | ||
|
|
045ab77508 | ||
|
|
ac5df0fab8 | ||
|
|
48822978c6 | ||
|
|
f061179646 | ||
|
|
f8a796ae04 | ||
|
|
aff52e0ef6 | ||
|
|
aef68d5ea4 | ||
|
|
cc1e70a641 | ||
|
|
f1843afc0f | ||
|
|
f8304c06f7 | ||
|
|
2396d44f10 | ||
|
|
1ee80b74e9 | ||
|
|
932e6ed82f | ||
|
|
b032dc09ac | ||
|
|
eb0b70bc5e | ||
|
|
238e35ae53 | ||
|
|
5b7fe94d4d | ||
|
|
43594398f0 | ||
|
|
2eef939b5a | ||
|
|
36f3661b98 | ||
|
|
4f3ee595e7 | ||
|
|
99f72aa631 | ||
|
|
9e5c0c558a | ||
|
|
b3b950f2d8 | ||
|
|
5dcf10a9fd | ||
|
|
3ef6c4ffed | ||
|
|
1ef8c5597a | ||
|
|
9b0aa46ff0 | ||
|
|
84171bef2d | ||
|
|
f19f13d44c | ||
|
|
4509da0966 | ||
|
|
38d5c6c7b6 | ||
|
|
be5ebd4017 | ||
|
|
44f9bcd9af | ||
|
|
f9ec92014c | ||
|
|
264af04ee3 | ||
|
|
2063507fb4 | ||
|
|
090cfc3bfd | ||
|
|
d1cdb47c3e | ||
|
|
237e5a7500 | ||
|
|
d339f4486d | ||
|
|
92025896b5 | ||
|
|
4eb214f7f1 | ||
|
|
706fb1c540 | ||
|
|
e37052c67d | ||
|
|
f8f6dc28b8 | ||
|
|
4472c81137 | ||
|
|
1e1463cf9a | ||
|
|
c21e44e086 | ||
|
|
e76616de83 | ||
|
|
92794a1822 | ||
|
|
ab0991f054 | ||
|
|
26c201b95b | ||
|
|
09693af5b9 | ||
|
|
f24b7e594b | ||
|
|
35c8ba5688 | ||
|
|
e2a34bd18a | ||
|
|
2f787889f7 | ||
|
|
9967cc0839 | ||
|
|
e47e881ae1 | ||
|
|
d9da100110 | ||
|
|
00cfe0c45b | ||
|
|
d92464b86c | ||
|
|
fec127c1af | ||
|
|
37ee71c663 | ||
|
|
6259e22480 | ||
|
|
474da1a5b5 | ||
|
|
6cee968a8a | ||
|
|
635c74d518 | ||
|
|
260d40f49b | ||
|
|
8f6ff33a39 | ||
|
|
5082f3c829 | ||
|
|
5d0568f759 | ||
|
|
9a78e159e7 | ||
|
|
503368623f | ||
|
|
735d304a20 | ||
|
|
29ddd78a80 | ||
|
|
b6c21216e3 | ||
|
|
6cc9d0db90 | ||
|
|
c6c201e575 |
@@ -10,3 +10,4 @@
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.ico binary
|
||||
*.blp binary
|
||||
|
||||
@@ -0,0 +1,658 @@
|
||||
-- Broodwatch brood: spawning, hit feedback, damage and death.
|
||||
|
||||
local _, BW = ...
|
||||
local CENTER = BW.CENTER
|
||||
local ROOM_RADIUS = BW.ROOM_RADIUS
|
||||
local SPAWN_MIN_DISTANCE = BW.SPAWN_MIN_DISTANCE
|
||||
local MAX_ALIVE = BW.MAX_ALIVE
|
||||
local DEATH_SECONDS = BW.DEATH_SECONDS
|
||||
local ANIM_STAND = BW.ANIM_STAND
|
||||
local ANIM_DEATH = BW.ANIM_DEATH
|
||||
local KINDS = BW.KINDS
|
||||
local ELITE = BW.ELITE
|
||||
local TUNING = BW.TUNING
|
||||
local VAMPIRIC_SHARE = BW.VAMPIRIC_SHARE
|
||||
local pools = BW.pools
|
||||
local enemies = BW.enemies
|
||||
local FloorAt = BW.FloorAt
|
||||
local Dist2 = BW.Dist2
|
||||
local DropGem = BW.DropGem
|
||||
local Mastered = BW.Mastered
|
||||
local SchoolColor = BW.SchoolColor
|
||||
local SPELLS = BW.SPELLS
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- The brood
|
||||
|
||||
-- The brood toughens as the run goes on, so late builds keep meeting
|
||||
-- resistance: with m the run's minutes over 5, health grows (1 + m)^3 (x8 at
|
||||
-- 5:00, x27 at 10:00) and bites (1 + m)^2 (x4 at 5:00, x9 at 10:00).
|
||||
local function KindStats(kind)
|
||||
local m = 1 + BW.run.time / 300;
|
||||
return math.floor(kind.hp * m * m * m + 0.5), kind.dps * m * m;
|
||||
end
|
||||
|
||||
-- Every aura on the enemy goes (its death, or its return from the pool).
|
||||
local function ClearAuras(e)
|
||||
if next(e.auras) then
|
||||
for spell in pairs(e.auras) do
|
||||
e.auras[spell] = nil;
|
||||
end
|
||||
e.actor:RemoveAuraVisual();
|
||||
end
|
||||
e.fearAura = nil;
|
||||
end
|
||||
|
||||
local function NewEnemy(kind)
|
||||
-- auras: the spell of each aura shown -> the time it ends (ShowAura).
|
||||
local e = { kind = kind, actor = BW.scene:AcquireActor(), auras = {} };
|
||||
if kind.path then
|
||||
e.actor:SetModelByPath(kind.path);
|
||||
else
|
||||
e.actor:SetModelByCreatureDisplayID(kind.display);
|
||||
end
|
||||
-- A whelp casts the next leap of Chain Lightning: its impact comes here.
|
||||
e.actor:HookScript("OnSpellVisualImpact", function(_, _, _, castID) BW.OnImpact(nil, nil, nil, castID); end);
|
||||
return e;
|
||||
end
|
||||
|
||||
-- A point in the room at least SPAWN_MIN_DISTANCE from the hero (ten tries).
|
||||
local function SpawnPoint()
|
||||
local hx, hy = BW.hero:GetPosition();
|
||||
local x, y;
|
||||
for _ = 1, 10 do
|
||||
local a = math.random() * 2 * math.pi;
|
||||
local r = ROOM_RADIUS * math.sqrt(math.random());
|
||||
x, y = CENTER[1] + r * math.cos(a), CENTER[2] + r * math.sin(a);
|
||||
if Dist2(x, y, hx, hy) >= SPAWN_MIN_DISTANCE * SPAWN_MIN_DISTANCE then
|
||||
break;
|
||||
end
|
||||
end
|
||||
return x, y;
|
||||
end
|
||||
|
||||
-- A whelp drops in a little above the floor and lands; an egg stands on it.
|
||||
-- (x, y): where, or a SpawnPoint.
|
||||
local function Spawn(kind, x, y)
|
||||
if not x then
|
||||
x, y = SpawnPoint();
|
||||
end
|
||||
local pool = pools[kind.key];
|
||||
local e = pool and table.remove(pool) or NewEnemy(kind);
|
||||
if e.sheepUntil then -- it died a sheep
|
||||
e.sheepUntil = nil;
|
||||
e.actor:SetModelByCreatureDisplayID(kind.display);
|
||||
end
|
||||
e.hp, e.dps = KindStats(kind);
|
||||
e.maxHp = e.hp;
|
||||
e.alive = true;
|
||||
e.dying = nil;
|
||||
e.fading = nil;
|
||||
e.dotLeft = nil;
|
||||
e.burnLeft = nil;
|
||||
e.slowUntil, e.slowFactor = nil, nil;
|
||||
e.stunUntil = nil;
|
||||
e.frozenAura = nil;
|
||||
e.fearUntil, e.fearTick = nil, nil;
|
||||
e.frozenUntil = nil;
|
||||
e.weakUntil, e.weakShare = nil, nil;
|
||||
e.taunter, e.tauntUntil = nil, nil;
|
||||
e.actState, e.actNext = nil, nil;
|
||||
ClearAuras(e);
|
||||
e.angle = math.random() * 2 * math.pi;
|
||||
local actor = e.actor;
|
||||
actor:SetAnimation(ANIM_STAND);
|
||||
actor:SetPaused(false, false); -- a crack held its last frame (the corpses, Step)
|
||||
if kind.speed == 0 then
|
||||
actor:SetPosition(x, y, FloorAt(x, y, CENTER[3]));
|
||||
actor:SetYaw(math.random() * 2 * math.pi);
|
||||
actor:Show();
|
||||
else
|
||||
actor:SetPosition(x, y, FloorAt(x, y, CENTER[3]) + 0.5);
|
||||
actor:Show();
|
||||
actor:SetMovementEnabled(true);
|
||||
actor:SetMovementSpeed("run", kind.speed);
|
||||
end
|
||||
tinsert(enemies, e);
|
||||
BW.run.alive = BW.run.alive + 1;
|
||||
BW.run.kindAlive[kind.key] = (BW.run.kindAlive[kind.key] or 0) + 1;
|
||||
return e;
|
||||
end
|
||||
|
||||
-- The enemy leaves the fight. A whelp's corpse keeps its movement until it
|
||||
-- is released: it stops where it is, plays Death, then lies Dead, as a dead
|
||||
-- unit does. A kind with a crack (an egg) plays it, with its sound.
|
||||
local function Retire(e)
|
||||
e.alive = false;
|
||||
BW.run.alive = BW.run.alive - 1;
|
||||
BW.run.kindAlive[e.kind.key] = BW.run.kindAlive[e.kind.key] - 1;
|
||||
e.actor:StopMoving();
|
||||
if e.kind.crackAnim then
|
||||
e.dying = e.kind.crackSeconds;
|
||||
e.actor:SetAnimation(e.kind.crackAnim);
|
||||
e.actor:PlaySound(e.kind.crackSound);
|
||||
else
|
||||
e.dying = DEATH_SECONDS;
|
||||
e.actor:SetAnimation(ANIM_DEATH);
|
||||
end
|
||||
ClearAuras(e);
|
||||
e.fearUntil, e.fearTick = nil, nil;
|
||||
end
|
||||
|
||||
local Wildfire -- a combo, below with the lasting effects
|
||||
|
||||
local function Kill(e)
|
||||
Retire(e);
|
||||
BW.run.kills = BW.run.kills + 1;
|
||||
local x, y, z = e.actor:GetPosition();
|
||||
DropGem(x, y, z, e.kind.xp, e.kind.gemScale);
|
||||
BW.DropItem(e, x, y, z);
|
||||
if e.kind == ELITE then
|
||||
BW.DropChest(x, y, z);
|
||||
end
|
||||
-- A corrupted enemy passes its Corruption on (in the step, after the loop).
|
||||
if e.dotLeft and e.dotSpread > 0 then
|
||||
tinsert(BW.run.spreads, e);
|
||||
end
|
||||
e.dotLeft = nil;
|
||||
if e.burnLeft and e.burnSchool == "Fire" then
|
||||
Wildfire(e, x, y);
|
||||
end
|
||||
e.burnLeft = nil;
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Hit feedback: a short flash on the model, and a number that rises from the
|
||||
-- hit point and fades.
|
||||
|
||||
local FLASH_SECONDS = 0.12
|
||||
local NUMBER_SECONDS = 0.8
|
||||
local NUMBER_RISE = 34 -- pixels over the number's life
|
||||
local NUMBER_FONT = "Fonts\\ARIALN.TTF"
|
||||
local BIG_HIT = 40 -- hits this large use the large size
|
||||
local MAX_NUMBERS = 80
|
||||
|
||||
local numbers = {} -- live numbers, oldest first
|
||||
local numberPool = {}
|
||||
|
||||
local function TakeNumber()
|
||||
local n = table.remove(numberPool);
|
||||
if n then
|
||||
return n;
|
||||
end
|
||||
if table.getn(numbers) >= MAX_NUMBERS then
|
||||
-- Too many at once: the oldest gives up its string.
|
||||
return table.remove(numbers, 1);
|
||||
end
|
||||
n = { text = BW.feedback:CreateFontString(nil, "OVERLAY") };
|
||||
n.text:SetFontObject(NumberFontNormal);
|
||||
return n;
|
||||
end
|
||||
|
||||
local function ShowNumber(e, amount, color, text)
|
||||
local x, y, z = e.actor:GetNamePosition();
|
||||
if not x then
|
||||
x, y, z = e.actor:GetPosition();
|
||||
z = z + 1.5;
|
||||
end
|
||||
local n = TakeNumber();
|
||||
n.x, n.y, n.z = x, y, z;
|
||||
n.age = 0;
|
||||
n.dx = math.random(-12, 12);
|
||||
local size = amount >= BIG_HIT and 22 or 16;
|
||||
if n.size ~= size then
|
||||
n.size = size;
|
||||
n.text:SetFont(NUMBER_FONT, size, "OUTLINE");
|
||||
end
|
||||
n.text:SetTextColor(color[1], color[2], color[3]);
|
||||
n.text:SetText(text or tostring(amount));
|
||||
n.text:SetAlpha(1);
|
||||
n.text:Show();
|
||||
tinsert(numbers, n);
|
||||
end
|
||||
|
||||
local function UpdateNumbers(dt)
|
||||
local count = table.getn(numbers);
|
||||
if count == 0 then
|
||||
return;
|
||||
end
|
||||
local scale = BW.scene:GetEffectiveScale();
|
||||
for i = count, 1, -1 do
|
||||
local n = numbers[i];
|
||||
n.age = n.age + dt;
|
||||
local sx, sy = BW.scene:Project3DPointTo2D(n.x, n.y, n.z);
|
||||
if n.age >= NUMBER_SECONDS or not sx then
|
||||
n.text:Hide();
|
||||
tinsert(numberPool, n);
|
||||
table.remove(numbers, i);
|
||||
else
|
||||
local t = n.age / NUMBER_SECONDS;
|
||||
n.text:ClearAllPoints();
|
||||
n.text:SetPoint("BOTTOM", BW.scene, "BOTTOMLEFT", sx / scale + n.dx, sy / scale + NUMBER_RISE * t);
|
||||
if t > 0.5 then
|
||||
n.text:SetAlpha(2 - 2 * t);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function ClearNumbers()
|
||||
for i = table.getn(numbers), 1, -1 do
|
||||
numbers[i].text:Hide();
|
||||
tinsert(numberPool, numbers[i]);
|
||||
numbers[i] = nil;
|
||||
end
|
||||
end
|
||||
|
||||
local function Flash(e)
|
||||
e.flash = FLASH_SECONDS;
|
||||
if not e.lit then
|
||||
e.lit = true;
|
||||
e.actor:SetHighlight("mouseover", true);
|
||||
end
|
||||
end
|
||||
|
||||
local function Unflash(e)
|
||||
if e.lit then
|
||||
e.lit = false;
|
||||
e.actor:SetHighlight("mouseover", false);
|
||||
end
|
||||
end
|
||||
|
||||
local function Damage(e, amount, color)
|
||||
if not e.alive then
|
||||
return;
|
||||
end
|
||||
-- Masteries: Corruption and Frost Nova's Shatter make their marks take more.
|
||||
if e.dotLeft and Mastered("corruption") then
|
||||
amount = amount * SPELLS.corruption.mastery.amplify;
|
||||
end
|
||||
if e.frozenUntil and BW.run.time < e.frozenUntil and Mastered("frostnova") then
|
||||
amount = amount * SPELLS.frostnova.mastery.amplify;
|
||||
end
|
||||
local dealt = math.floor(amount * BW.run.damageScale + 0.5);
|
||||
e.hp = e.hp - dealt;
|
||||
if BW.run.passives.vampiric then
|
||||
BW.run.leech = BW.run.leech + dealt * VAMPIRIC_SHARE * BW.run.passives.vampiric;
|
||||
end
|
||||
Flash(e);
|
||||
ShowNumber(e, dealt, color);
|
||||
if e.hp <= 0 then
|
||||
Kill(e);
|
||||
end
|
||||
end
|
||||
|
||||
-- Throws an enemy away from (x, y) at `knock` (out, up). A corpse flies too,
|
||||
-- in its death pose.
|
||||
local function Blast(e, x, y, knock)
|
||||
local share = e.kind.knockShare or 1;
|
||||
if share == 0 then
|
||||
return;
|
||||
end
|
||||
e.actor:KnockBack(x, y, knock[1] * share, knock[2] * share);
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Lasting effects: a hold (a root or a stun) keeps an enemy where it is, a
|
||||
-- fear sends it away from you, a weakness softens its bites. A hold or a fear
|
||||
-- shows the aura of the spell that caused it (ShowAura) for its own time,
|
||||
-- and the auras stack: a frozen whelp that is feared keeps its ice. A stun
|
||||
-- aura's kit pins Stun while the whelp stands. A kind takes its holdShare
|
||||
-- of a hold's, a fear's or a polymorph's time: elites shrug off part, and a
|
||||
-- kind with none (an egg) is immune.
|
||||
|
||||
local function ShowAura(e, spell, untilTime)
|
||||
if not spell then
|
||||
return;
|
||||
end
|
||||
if not e.auras[spell] then
|
||||
e.actor:AddAuraVisual(spell);
|
||||
end
|
||||
e.auras[spell] = math.max(e.auras[spell] or 0, untilTime);
|
||||
end
|
||||
|
||||
local function EndAura(e, spell)
|
||||
if spell and e.auras[spell] then
|
||||
e.auras[spell] = nil;
|
||||
e.actor:RemoveAuraVisual(spell);
|
||||
end
|
||||
end
|
||||
|
||||
-- A hold or a polymorph breaks a fear, its aura with it.
|
||||
local function EndFear(e)
|
||||
EndAura(e, e.fearAura);
|
||||
e.fearUntil, e.fearTick, e.fearAura = nil, nil, nil;
|
||||
end
|
||||
|
||||
local function HoldShare(e)
|
||||
return e.kind.holdShare or 1;
|
||||
end
|
||||
|
||||
-- `aura`: the spell whose aura shows; `frozen`: the hold is ice (Frost
|
||||
-- Nova's Shatter counts it).
|
||||
local function Hold(e, seconds, aura, frozen)
|
||||
if HoldShare(e) == 0 then
|
||||
return;
|
||||
end
|
||||
local untilTime = BW.run.time + seconds * HoldShare(e);
|
||||
e.stunUntil = math.max(e.stunUntil or 0, untilTime);
|
||||
if frozen then
|
||||
e.frozenUntil = e.stunUntil;
|
||||
e.frozenAura = aura;
|
||||
end
|
||||
EndFear(e);
|
||||
e.actor:StopMoving();
|
||||
ShowAura(e, aura, untilTime);
|
||||
end
|
||||
|
||||
local function Fear(e, seconds, aura)
|
||||
if HoldShare(e) == 0 then
|
||||
return;
|
||||
end
|
||||
local untilTime = BW.run.time + seconds * HoldShare(e);
|
||||
e.fearUntil = math.max(e.fearUntil or 0, untilTime);
|
||||
if aura then
|
||||
if e.fearAura ~= aura then
|
||||
EndAura(e, e.fearAura);
|
||||
end
|
||||
e.fearAura = aura;
|
||||
ShowAura(e, aura, untilTime);
|
||||
end
|
||||
end
|
||||
|
||||
-- An enemy's run speed: a sheep's amble, else its kind's, slowed.
|
||||
local function ApplySpeed(e)
|
||||
if e.kind.speed == 0 then
|
||||
return;
|
||||
end
|
||||
local speed = e.sheepUntil and TUNING.SHEEP_SPEED or e.kind.speed * (e.slowFactor or 1);
|
||||
e.actor:SetMovementSpeed("run", speed);
|
||||
end
|
||||
|
||||
-- Polymorph: the enemy is a sheep (SPELLS.polymorph.display) that wanders,
|
||||
-- harmless, until its time ends. Its own model comes back after.
|
||||
local function Polymorph(e, seconds)
|
||||
if HoldShare(e) == 0 then
|
||||
return;
|
||||
end
|
||||
if not e.sheepUntil then
|
||||
e.sheepUntil = 0;
|
||||
e.actor:SetModelByCreatureDisplayID(SPELLS.polymorph.display);
|
||||
e.actor:StopMoving();
|
||||
ApplySpeed(e);
|
||||
end
|
||||
EndFear(e);
|
||||
e.sheepUntil = math.max(e.sheepUntil, BW.run.time + seconds * HoldShare(e));
|
||||
end
|
||||
|
||||
local function Unpolymorph(e)
|
||||
e.sheepUntil = nil;
|
||||
e.actor:SetModelByCreatureDisplayID(e.kind.display);
|
||||
ApplySpeed(e);
|
||||
end
|
||||
|
||||
local function Weaken(e, share, seconds)
|
||||
e.weakShare = math.max(e.weakShare or 0, share);
|
||||
e.weakUntil = math.max(e.weakUntil or 0, BW.run.time + seconds);
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Burning: `dps` each second for `seconds`, of a spell's school (Pyroblast's
|
||||
-- fire, Moonfire's arcane), its ticks in the school's colour. A stronger burn takes over, a longer one lasts.
|
||||
-- A fire burn shows Immolate's aura (348: state kit 235, flames at the base).
|
||||
|
||||
local function Burn(e, dps, seconds, school)
|
||||
if not e.alive then
|
||||
return;
|
||||
end
|
||||
e.burnDps = math.max(e.burnLeft and e.burnDps or 0, dps);
|
||||
e.burnLeft = math.max(e.burnLeft or 0, seconds);
|
||||
e.burnSeconds = seconds;
|
||||
e.burnTick = e.burnTick or 1;
|
||||
e.burnSchool = school;
|
||||
e.burnColor = SchoolColor(school);
|
||||
if school == "Fire" then
|
||||
ShowAura(e, TUNING.BURN_AURA, BW.run.time + e.burnLeft);
|
||||
end
|
||||
end
|
||||
|
||||
local function EndBurn(e)
|
||||
if e.burnSchool == "Fire" then
|
||||
EndAura(e, TUNING.BURN_AURA);
|
||||
end
|
||||
e.burnLeft, e.burnTick = nil, nil;
|
||||
end
|
||||
|
||||
-- A frozen enemy's ice breaks (Shatter): its hold and its aura end.
|
||||
local function Thaw(e)
|
||||
EndAura(e, e.frozenAura);
|
||||
e.frozenUntil, e.stunUntil, e.frozenAura = nil, nil, nil;
|
||||
end
|
||||
|
||||
local function Announce(e, combo)
|
||||
ShowNumber(e, BIG_HIT, TUNING.COMBO_COLOR, combo .. "!");
|
||||
end
|
||||
|
||||
-- Wildfire: a burning enemy that dies sets the enemies around it alight
|
||||
-- with its burn (called from Kill).
|
||||
Wildfire = function(e, x, y)
|
||||
local r2 = TUNING.WILDFIRE_RADIUS * TUNING.WILDFIRE_RADIUS;
|
||||
local caught = false;
|
||||
for _, o in ipairs(enemies) do
|
||||
if o.alive and not o.burnLeft then
|
||||
local ox, oy = o.actor:GetPosition();
|
||||
if Dist2(ox, oy, x, y) <= r2 then
|
||||
Burn(o, e.burnDps, e.burnSeconds, "Fire");
|
||||
caught = true;
|
||||
end
|
||||
end
|
||||
end
|
||||
if caught then
|
||||
Announce(e, "Wildfire");
|
||||
end
|
||||
end
|
||||
|
||||
-- A spell's hit, by its school (def.school, the spell's own) against what
|
||||
-- the enemy suffers. The combos:
|
||||
-- - Shatter: any hit but frost on a frozen enemy deals SHATTER times its
|
||||
-- damage and breaks the ice.
|
||||
-- - Dread: shadow hurts a fleeing enemy DREAD times as much.
|
||||
-- - Detonate: nature sets off a burn: what is left of it lands at once,
|
||||
-- and half of that on every enemy within DETONATE_RADIUS.
|
||||
-- - Steam: frost puts out a fire burn and stuns the enemy for STEAM_STUN.
|
||||
-- - Wildfire (Kill): a burning enemy that dies sets its neighbours alight.
|
||||
local function SpellDamage(e, amount, def)
|
||||
if not e.alive then
|
||||
return;
|
||||
end
|
||||
local school = def.school;
|
||||
amount = amount * BW.GearScale(def); -- Gear.lua
|
||||
local combo, detonated, steamed;
|
||||
if school ~= "Frost" and e.frozenUntil and BW.run.time < e.frozenUntil then
|
||||
amount = amount * TUNING.SHATTER;
|
||||
Thaw(e);
|
||||
combo = "Shatter";
|
||||
elseif school == "Shadow" and e.fearUntil then
|
||||
amount = amount * TUNING.DREAD;
|
||||
end
|
||||
if e.burnLeft then
|
||||
if school == "Nature" then
|
||||
detonated = e.burnDps * e.burnLeft;
|
||||
EndBurn(e);
|
||||
amount = amount + detonated;
|
||||
combo = "Detonate";
|
||||
elseif school == "Frost" and e.burnSchool == "Fire" then
|
||||
EndBurn(e);
|
||||
steamed = true;
|
||||
combo = "Steam";
|
||||
end
|
||||
end
|
||||
local x, y = e.actor:GetPosition();
|
||||
Damage(e, amount, def.hitColor);
|
||||
if combo then
|
||||
Announce(e, combo);
|
||||
end
|
||||
if steamed and e.alive then
|
||||
Hold(e, TUNING.STEAM_STUN, TUNING.STUN_AURA);
|
||||
end
|
||||
BW.OnSpellHit(e, def); -- a weapon's proc (Gear.lua)
|
||||
if detonated then
|
||||
local r2 = TUNING.DETONATE_RADIUS * TUNING.DETONATE_RADIUS;
|
||||
for _, o in ipairs(enemies) do
|
||||
if o.alive and o ~= e then
|
||||
local ox, oy = o.actor:GetPosition();
|
||||
if Dist2(ox, oy, x, y) <= r2 then
|
||||
Damage(o, detonated * 0.5, def.hitColor);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function TickBurn(e, dt)
|
||||
e.burnTick = e.burnTick - dt;
|
||||
if e.burnTick > 0 then
|
||||
return;
|
||||
end
|
||||
e.burnTick = e.burnTick + 1;
|
||||
e.burnLeft = e.burnLeft - 1;
|
||||
Damage(e, e.burnDps, e.burnColor);
|
||||
if e.alive and e.burnLeft <= 0 then
|
||||
EndBurn(e);
|
||||
end
|
||||
end
|
||||
|
||||
-- Ends the effects whose time has run out (the step, for each live enemy),
|
||||
-- and burns the fleeing with Psychic Scream's Terror.
|
||||
local function UpdateStates(e, dt)
|
||||
local t = BW.run.time;
|
||||
if e.burnLeft then
|
||||
TickBurn(e, dt);
|
||||
if not e.alive then
|
||||
return;
|
||||
end
|
||||
end
|
||||
if e.fearUntil and Mastered("scream") then
|
||||
e.fearTick = (e.fearTick or 1) - dt;
|
||||
if e.fearTick <= 0 then
|
||||
e.fearTick = e.fearTick + 1;
|
||||
Damage(e, SPELLS.scream.mastery.damage, SPELLS.scream.hitColor);
|
||||
if not e.alive then
|
||||
return;
|
||||
end
|
||||
end
|
||||
end
|
||||
for spell, untilTime in pairs(e.auras) do
|
||||
if t >= untilTime then -- clearing a key during `pairs` is allowed
|
||||
e.auras[spell] = nil;
|
||||
e.actor:RemoveAuraVisual(spell);
|
||||
end
|
||||
end
|
||||
if e.fearUntil and t >= e.fearUntil then
|
||||
EndFear(e);
|
||||
end
|
||||
if e.weakUntil and t >= e.weakUntil then
|
||||
e.weakUntil, e.weakShare = nil, nil;
|
||||
end
|
||||
if e.sheepUntil and t >= e.sheepUntil then
|
||||
Unpolymorph(e);
|
||||
end
|
||||
end
|
||||
|
||||
-- The share of an enemy's bite that lands.
|
||||
local function BiteShare(e)
|
||||
return e.weakShare and 1 - e.weakShare or 1;
|
||||
end
|
||||
|
||||
local function Release(i)
|
||||
local e = enemies[i];
|
||||
Unflash(e);
|
||||
e.actor:SetMovementEnabled(false);
|
||||
e.actor:Hide();
|
||||
local pool = pools[e.kind.key];
|
||||
if not pool then
|
||||
pool = {};
|
||||
pools[e.kind.key] = pool;
|
||||
end
|
||||
tinsert(pool, e);
|
||||
table.remove(enemies, i);
|
||||
end
|
||||
|
||||
local function ClearEnemies()
|
||||
for i = table.getn(enemies), 1, -1 do
|
||||
if enemies[i].alive then
|
||||
BW.run.alive = BW.run.alive - 1;
|
||||
end
|
||||
Release(i);
|
||||
end
|
||||
end
|
||||
|
||||
-- How many should be alive at this second of the run.
|
||||
local function TargetAlive(t)
|
||||
return math.min(MAX_ALIVE, 8 + math.floor(t * 0.38));
|
||||
end
|
||||
|
||||
local function PickKind()
|
||||
local open = 0;
|
||||
for _, kind in ipairs(KINDS) do
|
||||
if BW.run.time >= kind.from then
|
||||
open = open + 1;
|
||||
end
|
||||
end
|
||||
-- Newer kinds are rarer: the first kind open gets the most weight. A
|
||||
-- kind at its `max` gives way to the first.
|
||||
local roll = math.random() * (open * (open + 1) / 2);
|
||||
for i = 1, open do
|
||||
local w = open - i + 1;
|
||||
if roll < w then
|
||||
local kind = KINDS[i];
|
||||
if kind.max and (BW.run.kindAlive[kind.key] or 0) >= kind.max then
|
||||
return KINDS[1];
|
||||
end
|
||||
return kind;
|
||||
end
|
||||
roll = roll - w;
|
||||
end
|
||||
return KINDS[1];
|
||||
end
|
||||
|
||||
-- The kind of a key (KINDS, or the elite's), for the slash command.
|
||||
local function KindByKey(key)
|
||||
if key == ELITE.key then
|
||||
return ELITE;
|
||||
end
|
||||
for _, kind in ipairs(KINDS) do
|
||||
if kind.key == key then
|
||||
return kind;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- For the files that load after this one.
|
||||
BW.KindByKey = KindByKey
|
||||
BW.Blast = Blast
|
||||
BW.Hold = Hold
|
||||
BW.ShowAura = ShowAura
|
||||
BW.Fear = Fear
|
||||
BW.HoldShare = HoldShare
|
||||
BW.Weaken = Weaken
|
||||
BW.Burn = Burn
|
||||
BW.SpellDamage = SpellDamage
|
||||
BW.Polymorph = Polymorph
|
||||
BW.ApplySpeed = ApplySpeed
|
||||
BW.UpdateStates = UpdateStates
|
||||
BW.BiteShare = BiteShare
|
||||
BW.SpawnPoint = SpawnPoint
|
||||
BW.Spawn = Spawn
|
||||
BW.Retire = Retire
|
||||
BW.Kill = Kill
|
||||
BW.ShowNumber = ShowNumber
|
||||
BW.UpdateNumbers = UpdateNumbers
|
||||
BW.ClearNumbers = ClearNumbers
|
||||
BW.Flash = Flash
|
||||
BW.Unflash = Unflash
|
||||
BW.Damage = Damage
|
||||
BW.Release = Release
|
||||
BW.ClearEnemies = ClearEnemies
|
||||
BW.TargetAlive = TargetAlive
|
||||
BW.PickKind = PickKind
|
||||
@@ -0,0 +1,744 @@
|
||||
-- Broodwatch: hold Onyxia's Lair against an endless brood.
|
||||
--
|
||||
-- You run with W A S D (in screen directions). Your spells cast themselves.
|
||||
-- Whelps pour into the lair and chase you; each kill drops a gem, and gems
|
||||
-- level you up. At each level you pick one of three upgrades (click, or
|
||||
-- press 1 2 3). An elite dragonspawn joins every 90 seconds. Survive ten
|
||||
-- minutes to win.
|
||||
--
|
||||
-- /broodwatch starts a run. P pauses. Escape leaves.
|
||||
--
|
||||
-- Prototype: one ClassicAPI ModelScene, the lair as its WMO, every unit an
|
||||
-- actor. The frame time and the Lua time per frame show in the corner.
|
||||
|
||||
local _, BW = ...
|
||||
local Blast = BW.Blast
|
||||
local TUNING = BW.TUNING
|
||||
local LAIR_MAP = BW.LAIR_MAP
|
||||
local CENTER = BW.CENTER
|
||||
local FAR_CLIP = BW.FAR_CLIP
|
||||
local CAMERA_YAW = BW.CAMERA_YAW
|
||||
local CAMERA_PITCH = BW.CAMERA_PITCH
|
||||
local CAMERA_ZOOM = BW.CAMERA_ZOOM
|
||||
local RUN_SECONDS = BW.RUN_SECONDS
|
||||
local ELITE_EVERY = BW.ELITE_EVERY
|
||||
local RETARGET_SECONDS = BW.RETARGET_SECONDS
|
||||
local SURROUND_RADIUS = BW.SURROUND_RADIUS
|
||||
local HERO_HP = BW.HERO_HP
|
||||
local MAGNET = BW.MAGNET
|
||||
local GEM_SPEED = BW.GEM_SPEED
|
||||
local GEM_TAKE = BW.GEM_TAKE
|
||||
local ELITE = BW.ELITE
|
||||
local SPELLS = BW.SPELLS
|
||||
local MAX_LEVEL = BW.MAX_LEVEL
|
||||
local PASSIVES = BW.PASSIVES
|
||||
local SOULSTONE_SPELL = BW.SOULSTONE_SPELL
|
||||
local SOULSTONE_BLAST = BW.SOULSTONE_BLAST
|
||||
local AT_ONCE = BW.AT_ONCE
|
||||
local enemies = BW.enemies
|
||||
local gemPool = BW.gemPool
|
||||
local gems = BW.gems
|
||||
local Dist2 = BW.Dist2
|
||||
local Clock = BW.Clock
|
||||
local ClearGems = BW.ClearGems
|
||||
local Spawn = BW.Spawn
|
||||
local Kill = BW.Kill
|
||||
local ShowNumber = BW.ShowNumber
|
||||
local UpdateNumbers = BW.UpdateNumbers
|
||||
local ClearNumbers = BW.ClearNumbers
|
||||
local Unflash = BW.Unflash
|
||||
local Damage = BW.Damage
|
||||
local Release = BW.Release
|
||||
local ClearEnemies = BW.ClearEnemies
|
||||
local TargetAlive = BW.TargetAlive
|
||||
local PickKind = BW.PickKind
|
||||
local Stat = BW.Stat
|
||||
local Nearest = BW.Nearest
|
||||
local UpdateTotems = BW.UpdateTotems
|
||||
local ClearTotems = BW.ClearTotems
|
||||
local Unslow = BW.Unslow
|
||||
local UpdateAreas = BW.UpdateAreas
|
||||
local UpdateLandings = BW.UpdateLandings
|
||||
local ClearLandings = BW.ClearLandings
|
||||
local ClearAreas = BW.ClearAreas
|
||||
local Fire = BW.Fire
|
||||
local CastNext = BW.CastNext
|
||||
local Corrupt = BW.Corrupt
|
||||
local UpdateBuild = BW.UpdateBuild
|
||||
local SetPaused = BW.SetPaused
|
||||
local MaxHp = BW.MaxHp
|
||||
local UpdateHealth = BW.UpdateHealth
|
||||
local GainXp = BW.GainXp
|
||||
local Taunted = BW.Taunted
|
||||
local UpdateItems = BW.UpdateItems
|
||||
local ClearItems = BW.ClearItems
|
||||
local UpdateChests = BW.UpdateChests
|
||||
local ClearChests = BW.ClearChests
|
||||
local UpdateEggs = BW.UpdateEggs
|
||||
local ClearEggs = BW.ClearEggs
|
||||
local UpdateMines = BW.UpdateMines
|
||||
local ClearMines = BW.ClearMines
|
||||
local UpdateVents = BW.UpdateVents
|
||||
local UpdateStates = BW.UpdateStates
|
||||
local BiteShare = BW.BiteShare
|
||||
local UpdateWards = BW.UpdateWards
|
||||
local ClearWards = BW.ClearWards
|
||||
local UpdatePet = BW.UpdatePet
|
||||
local ClearPet = BW.ClearPet
|
||||
local UpdateChannels = BW.UpdateChannels
|
||||
local ClearChannels = BW.ClearChannels
|
||||
local Zap = BW.Zap
|
||||
local UpdateInfernals = BW.UpdateInfernals
|
||||
local ClearInfernals = BW.ClearInfernals
|
||||
local EndShield = BW.EndShield
|
||||
local Mastered = BW.Mastered
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Movement: W A S D in screen directions. The hero faces the way it runs.
|
||||
|
||||
local function Held(key)
|
||||
return BW.held[key] and 1 or 0;
|
||||
end
|
||||
|
||||
-- The eight moves about the facing, from straight ahead counter-clockwise
|
||||
-- in eighths of a turn: { forward or back, side (left positive) }.
|
||||
local OCTANTS = { { 1, 0 }, { 1, 1 }, { 0, 1 }, { -1, 1 }, { -1, 0 }, { -1, -1 }, { 0, -1 }, { 1, -1 } }
|
||||
|
||||
local function Steer()
|
||||
-- A charge steers itself (Kinds.lua).
|
||||
if not BW.run or BW.paused or BW.run.over or BW.run.charge then
|
||||
return;
|
||||
end
|
||||
local forward = Held("W") - Held("S");
|
||||
local left = Held("A") - Held("D");
|
||||
local pose = BW.run.pose and BW.run.poseYaw;
|
||||
if forward == 0 and left == 0 then
|
||||
BW.Drive(0, 0, pose);
|
||||
return;
|
||||
end
|
||||
local yaw = BW.camera:GetYaw() + math.atan2(left, forward);
|
||||
BW.run.heroYaw = yaw;
|
||||
if pose then
|
||||
-- A cone's cast holds you facing it: you go the way you hold by the
|
||||
-- nearest of the eight moves a character makes about its facing.
|
||||
local off = math.mod(yaw - pose + 3 * math.pi, 2 * math.pi) - math.pi;
|
||||
local octant = OCTANTS[math.mod(math.floor(off / (math.pi / 4) + 0.5) + 8, 8) + 1];
|
||||
BW.Drive(octant[1], octant[2], pose);
|
||||
else
|
||||
BW.Drive(1, 0, yaw);
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- The run
|
||||
|
||||
local function EndRun(won)
|
||||
BW.run.over = true;
|
||||
SetPaused(true);
|
||||
BW.cards:Hide();
|
||||
ClearNumbers();
|
||||
BW.run.hurtGlow = 0;
|
||||
BW.hurtTexture:SetAlpha(0);
|
||||
if won then
|
||||
BW.bannerText:SetText("Victory! You held the lair for " .. Clock(RUN_SECONDS));
|
||||
BW.bannerText:SetTextColor(0.25, 1, 0.25);
|
||||
else
|
||||
BW.bannerText:SetText("You fell at " .. Clock(BW.run.time));
|
||||
BW.bannerText:SetTextColor(1, 0.25, 0.25);
|
||||
end
|
||||
BW.hintText:SetText(string.format("Level %d, %d kills. R: try again. Escape: leave.", BW.run.level, BW.run.kills));
|
||||
end
|
||||
|
||||
BW.heroNumber = {} -- ShowNumber's stand-in record for the hero
|
||||
local HEAL_COLOR = { 0.3, 1, 0.3 }
|
||||
|
||||
-- A new run. It opens on the pick of its first spell, unless `spells` (a
|
||||
-- test's, from the slash command) gives its spells.
|
||||
local function StartRun(spells)
|
||||
if BW.run then
|
||||
ClearEnemies();
|
||||
ClearGems();
|
||||
ClearNumbers();
|
||||
ClearTotems();
|
||||
ClearAreas();
|
||||
ClearLandings();
|
||||
ClearItems();
|
||||
ClearChests();
|
||||
ClearEggs();
|
||||
ClearMines();
|
||||
ClearWards();
|
||||
ClearPet();
|
||||
ClearChannels();
|
||||
ClearInfernals();
|
||||
BW.ClearKinds();
|
||||
BW.ClearFoes();
|
||||
end
|
||||
BW.hero:RemoveAuraVisual();
|
||||
BW.hurtTexture:SetAlpha(0);
|
||||
BW.run = {
|
||||
time = 0, kills = 0, alive = 0, kindAlive = {}, level = 1, xp = 0, pendingLevels = 0, chests = 0, chestPicks = 0,
|
||||
nextClutch = TUNING.EGG_FIRST,
|
||||
hp = HERO_HP, damageScale = 1, heroYaw = 0,
|
||||
spells = spells or {}, startPick = not spells,
|
||||
passives = {},
|
||||
castQueue = {}, castGap = 0, inFlight = {},
|
||||
spawnGap = 0, nextElite = ELITE_EVERY, retargetCarry = 0, retargetCursor = 1,
|
||||
shownSecond = -1, perfFrames = 0, perfLua = 0, perfClock = 0, hurtGlow = 0,
|
||||
spreads = {}, pendingHeal = 0, pendingHurt = 0, leech = 0, landings = {},
|
||||
gear = {}, gearReady = {}, gearSlots = {}, procReady = 0, shotLeft = 0,
|
||||
};
|
||||
SetPaused(false);
|
||||
BW.held = {};
|
||||
BW.moving = false;
|
||||
BW.moveFwd, BW.moveSide, BW.moveFacing = 0, 0, CAMERA_YAW;
|
||||
BW.hero:SetMovementEnabled(false);
|
||||
BW.hero:SetPosition(CENTER[1], CENTER[2], CENTER[3] + 0.5);
|
||||
BW.hero:SetYaw(CAMERA_YAW);
|
||||
BW.hero:Show();
|
||||
BW.hero:SetAlpha(1); -- a grace may have faded it
|
||||
BW.ResetGear(); -- your own weapons again
|
||||
BW.hero:SetMovementEnabled(true);
|
||||
BW.run.heroRun = BW.hero:GetMovementSpeed("run");
|
||||
BW.LayMines(); -- away from the hero, so after it stands at the centre
|
||||
BW.ResetVents();
|
||||
BW.cards:Hide();
|
||||
BW.bannerText:SetText("");
|
||||
BW.hintText:SetText("");
|
||||
BW.UpdateXp(AT_ONCE);
|
||||
UpdateHealth(AT_ONCE);
|
||||
UpdateBuild();
|
||||
BW.camera:SetZoomDistance(CAMERA_ZOOM);
|
||||
BW.camera:SnapAllInterpolatedValues();
|
||||
BW.ShowCards(); -- the first spell's pick
|
||||
end
|
||||
|
||||
local function IsCorrupted(e)
|
||||
return e.dotLeft ~= nil;
|
||||
end
|
||||
|
||||
-- Sends a slice of the brood after the hero, so each one gets a new MoveTo
|
||||
-- every RETARGET_SECONDS, at its own point of a ring around the hero (or,
|
||||
-- for a kind that keeps its distance, a point that far away). One busy with
|
||||
-- an act moves as the act says (Foes.lua).
|
||||
local function Chase(dt, hx, hy, hz)
|
||||
local n = table.getn(enemies);
|
||||
if n == 0 then
|
||||
return;
|
||||
end
|
||||
BW.run.retargetCarry = BW.run.retargetCarry + n * dt / RETARGET_SECONDS;
|
||||
while BW.run.retargetCarry >= 1 do
|
||||
BW.run.retargetCarry = BW.run.retargetCarry - 1;
|
||||
if BW.run.retargetCursor > n then
|
||||
BW.run.retargetCursor = 1;
|
||||
end
|
||||
local e = enemies[BW.run.retargetCursor];
|
||||
if e.alive and e.kind.speed > 0 and not (e.stunUntil and BW.run.time < e.stunUntil) and not e.actor:IsFalling()
|
||||
and not BW.Acting(e) then
|
||||
if e.sheepUntil then
|
||||
-- A sheep grazes about where it is.
|
||||
local x, y = e.actor:GetPosition();
|
||||
local a = math.random() * 2 * math.pi;
|
||||
e.actor:MoveTo(x + TUNING.SHEEP_WANDER * math.cos(a), y + TUNING.SHEEP_WANDER * math.sin(a), hz);
|
||||
elseif e.fearUntil then
|
||||
local x, y = e.actor:GetPosition();
|
||||
local dx, dy = x - hx, y - hy;
|
||||
local d = math.sqrt(dx * dx + dy * dy);
|
||||
if d < 0.1 then
|
||||
dx, dy, d = math.cos(e.angle), math.sin(e.angle), 1;
|
||||
end
|
||||
local step = TUNING.FEAR_STEP / d;
|
||||
e.actor:MoveTo(x + dx * step, y + dy * step, hz);
|
||||
elseif Taunted(e) then
|
||||
local cx, cy, cz = e.taunter.actor:GetPosition();
|
||||
e.actor:MoveTo(cx + SURROUND_RADIUS * math.cos(e.angle), cy + SURROUND_RADIUS * math.sin(e.angle), cz);
|
||||
elseif e.kind.keep then
|
||||
local px, py = BW.KeepPoint(e, hx, hy);
|
||||
e.actor:MoveTo(px, py, hz);
|
||||
else
|
||||
e.actor:MoveTo(hx + SURROUND_RADIUS * math.cos(e.angle), hy + SURROUND_RADIUS * math.sin(e.angle), hz);
|
||||
end
|
||||
end
|
||||
BW.run.retargetCursor = BW.run.retargetCursor + 1;
|
||||
end
|
||||
end
|
||||
|
||||
local function Step(dt)
|
||||
BW.run.time = BW.run.time + dt;
|
||||
local hx, hy, hz = BW.hero:GetPosition();
|
||||
|
||||
-- Spawns.
|
||||
BW.run.spawnGap = BW.run.spawnGap - dt;
|
||||
if BW.run.spawnGap <= 0 then
|
||||
BW.run.spawnGap = 0.1;
|
||||
local want = TargetAlive(BW.run.time) - BW.run.alive;
|
||||
for _ = 1, math.min(want, 5) do
|
||||
Spawn(PickKind());
|
||||
end
|
||||
end
|
||||
if BW.run.time >= BW.run.nextElite then
|
||||
BW.run.nextElite = BW.run.nextElite + ELITE_EVERY;
|
||||
for _ = 1, 1 + math.floor(BW.run.time / 180) do
|
||||
Spawn(ELITE);
|
||||
end
|
||||
end
|
||||
UpdateEggs();
|
||||
UpdateMines(dt, hx, hy);
|
||||
UpdateVents(dt, hx, hy);
|
||||
BW.UpdateMarkers();
|
||||
|
||||
Chase(dt, hx, hy, hz);
|
||||
BW.UpdateFoes(dt, hx, hy, hz);
|
||||
|
||||
-- Contact damage, and the corpses. Thorns burns the biters once a second.
|
||||
local hurt = 0;
|
||||
local thorns = false;
|
||||
if BW.run.thornsUntil then
|
||||
if BW.run.time >= BW.run.thornsUntil then
|
||||
BW.run.thornsUntil = nil;
|
||||
else
|
||||
BW.run.thornsTick = BW.run.thornsTick - dt;
|
||||
if BW.run.thornsTick <= 0 then
|
||||
BW.run.thornsTick = BW.run.thornsTick + 1;
|
||||
thorns = true;
|
||||
end
|
||||
end
|
||||
end
|
||||
for i = table.getn(enemies), 1, -1 do
|
||||
local e = enemies[i];
|
||||
if e.lit then
|
||||
e.flash = e.flash - dt;
|
||||
if e.flash <= 0 then
|
||||
Unflash(e);
|
||||
end
|
||||
end
|
||||
if e.alive and e.dotLeft then
|
||||
e.dotTick = e.dotTick - dt;
|
||||
if e.dotTick <= 0 then
|
||||
e.dotTick = e.dotTick + 1;
|
||||
e.dotLeft = e.dotLeft - 1;
|
||||
-- The last tick can kill too, and then it still spreads (Kill
|
||||
-- reads dotLeft), so it ends after the damage.
|
||||
Damage(e, e.dotDamage, SPELLS[e.dotKey].hitColor);
|
||||
if e.dotHeal then -- Siphon Life
|
||||
BW.run.pendingHeal = BW.run.pendingHeal + e.dotHeal;
|
||||
end
|
||||
if e.dotLeft and e.dotLeft <= 0 then
|
||||
e.dotLeft = nil;
|
||||
end
|
||||
end
|
||||
end
|
||||
if e.alive and e.slowUntil and BW.run.time >= e.slowUntil then
|
||||
Unslow(e);
|
||||
end
|
||||
if e.alive then
|
||||
UpdateStates(e, dt);
|
||||
end
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
local reach = e.kind.reach;
|
||||
if e.kind.dps > 0 and not Taunted(e) and not e.fearUntil and not e.sheepUntil and Dist2(x, y, hx, hy) <= reach * reach then
|
||||
hurt = hurt + e.dps * BiteShare(e) * (1 - PASSIVES.devotion.per * (BW.run.passives.devotion or 0) / 100);
|
||||
Zap(e);
|
||||
if thorns then
|
||||
Damage(e, BW.run.thornsDamage, SPELLS.thorns.hitColor);
|
||||
if Mastered("thorns") then
|
||||
BW.run.pendingHeal = BW.run.pendingHeal + SPELLS.thorns.mastery.heal;
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
-- The corpse lies, then fades out as the world's do, and goes
|
||||
-- once the fade has hidden it. A cracked egg holds the crack's
|
||||
-- last frame while it fades.
|
||||
e.dying = e.dying - dt;
|
||||
if e.dying <= 0 then
|
||||
if not e.fading then
|
||||
e.fading = true;
|
||||
if e.kind.crackAnim then
|
||||
e.actor:SetPaused(true, false);
|
||||
end
|
||||
e.actor:FadeOut();
|
||||
elseif not e.actor:IsShown() then
|
||||
Release(i);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Corruption passes from each enemy that died with it to its neighbours.
|
||||
for i = table.getn(BW.run.spreads), 1, -1 do
|
||||
local from = BW.run.spreads[i];
|
||||
BW.run.spreads[i] = nil;
|
||||
local fx, fy = from.actor:GetPosition();
|
||||
for _ = 1, from.dotSpread do
|
||||
local e = Nearest(fx, fy, SPELLS[from.dotKey].hop, IsCorrupted);
|
||||
if not e then
|
||||
break;
|
||||
end
|
||||
Corrupt(e, from.dotDamage, from.dotSeconds, from.dotSpread, from.dotKey, from.dotHeal);
|
||||
end
|
||||
end
|
||||
UpdateTotems(dt);
|
||||
UpdateAreas(dt);
|
||||
UpdateLandings(dt);
|
||||
UpdateItems(dt, hx, hy, hz);
|
||||
UpdateChests(dt, hx, hy);
|
||||
UpdateWards(dt);
|
||||
BW.UpdateGear(dt, hx, hy);
|
||||
UpdatePet(dt, hx, hy, hz);
|
||||
UpdateChannels(dt);
|
||||
UpdateInfernals(dt);
|
||||
BW.UpdateKinds(dt);
|
||||
if BW.run.shoutUntil and BW.run.time >= BW.run.shoutUntil then
|
||||
BW.run.shoutUntil = nil;
|
||||
BW.ApplyHeroSpeed();
|
||||
end
|
||||
|
||||
-- Power Word: Shield soaks the bites first, and the hurt of your own
|
||||
-- spells (Hellfire's fire).
|
||||
if hurt > 0 then
|
||||
BW.HurtHero(hurt * dt, "Physical");
|
||||
end
|
||||
local taken = BW.run.pendingHurt;
|
||||
BW.run.pendingHurt = 0;
|
||||
if BW.run.graceUntil then -- the grace after the cards (Hud.lua)
|
||||
if BW.run.time < BW.run.graceUntil then
|
||||
taken = 0;
|
||||
else
|
||||
BW.run.graceUntil = nil;
|
||||
BW.hero:SetAlpha(1);
|
||||
end
|
||||
end
|
||||
if BW.run.shield then
|
||||
local soak = math.min(BW.run.shield, taken);
|
||||
BW.run.shield = BW.run.shield - soak;
|
||||
taken = taken - soak;
|
||||
if BW.run.shield <= 0 then
|
||||
EndShield(true);
|
||||
elseif BW.run.time >= BW.run.shieldUntil then
|
||||
EndShield();
|
||||
elseif soak > 0 then
|
||||
UpdateHealth();
|
||||
end
|
||||
end
|
||||
|
||||
-- The screen's edge glows red while the brood bites, and fades after.
|
||||
local glow = BW.run.hurtGlow;
|
||||
if taken > 0 then
|
||||
glow = math.min(0.8, glow + 4 * dt);
|
||||
elseif glow > 0 then
|
||||
glow = math.max(0, glow - 1.5 * dt);
|
||||
end
|
||||
if glow ~= BW.run.hurtGlow then
|
||||
BW.run.hurtGlow = glow;
|
||||
BW.hurtTexture:SetAlpha(glow);
|
||||
end
|
||||
|
||||
if BW.run.leech > 0 then
|
||||
-- Vampiric Embrace heals at most its rate; the rest of a burst is
|
||||
-- kept for at most a second.
|
||||
local rate = PASSIVES.vampiric.per * (BW.run.passives.vampiric or 0);
|
||||
BW.run.leech = math.min(BW.run.leech, rate);
|
||||
local heal = math.min(BW.run.leech, rate * dt);
|
||||
BW.run.leech = BW.run.leech - heal;
|
||||
if heal > 0 and BW.run.hp < MaxHp() then
|
||||
BW.run.hp = math.min(MaxHp(), BW.run.hp + heal);
|
||||
UpdateHealth();
|
||||
end
|
||||
end
|
||||
if BW.run.pendingHeal >= 1 then
|
||||
-- Whole points heal; the fraction waits for more (Siphon Life heals
|
||||
-- 0.3 a tick).
|
||||
local heal = math.floor(BW.run.pendingHeal);
|
||||
BW.run.pendingHeal = BW.run.pendingHeal - heal;
|
||||
if heal > 0 and BW.run.hp < MaxHp() then
|
||||
BW.run.hp = math.min(MaxHp(), BW.run.hp + heal);
|
||||
UpdateHealth();
|
||||
BW.heroNumber.actor = BW.hero;
|
||||
ShowNumber(BW.heroNumber, heal, HEAL_COLOR, "+" .. heal);
|
||||
end
|
||||
end
|
||||
|
||||
local regen = PASSIVES.renewal.per * (BW.run.passives.renewal or 0);
|
||||
if taken > 0 or (regen > 0 and BW.run.hp < MaxHp()) then
|
||||
BW.run.hp = math.min(MaxHp(), BW.run.hp + regen * dt - taken);
|
||||
UpdateHealth();
|
||||
if BW.run.hp <= 0 and BW.run.passives.soulstone and not BW.run.soulstoneUsed then
|
||||
-- Rise again at half health, and throw back the brood close by.
|
||||
BW.run.soulstoneUsed = true;
|
||||
BW.run.hp = MaxHp() * 0.5;
|
||||
UpdateHealth();
|
||||
BW.hero:PlaySpellVisual(SOULSTONE_SPELL, BW.hero, 0);
|
||||
local r2 = SOULSTONE_BLAST * SOULSTONE_BLAST;
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
if Dist2(x, y, hx, hy) <= r2 then
|
||||
if e.kind ~= ELITE then
|
||||
Kill(e);
|
||||
end
|
||||
Blast(e, hx, hy, TUNING.SOULSTONE_KNOCK);
|
||||
end
|
||||
end
|
||||
end
|
||||
UpdateBuild();
|
||||
end
|
||||
if BW.run.hp <= 0 then
|
||||
EndRun(false);
|
||||
return;
|
||||
end
|
||||
end
|
||||
|
||||
-- Gems.
|
||||
local magnet = MAGNET * (1 + 0.4 * (BW.run.passives.reach or 0));
|
||||
local m2 = magnet * magnet;
|
||||
local take2 = GEM_TAKE * GEM_TAKE;
|
||||
local gained = 0;
|
||||
for i = table.getn(gems), 1, -1 do
|
||||
local gem = gems[i];
|
||||
local d2 = Dist2(gem.x, gem.y, hx, hy);
|
||||
if not gem.pulled and d2 <= m2 then
|
||||
gem.pulled = true;
|
||||
end
|
||||
if gem.pulled then
|
||||
if d2 <= take2 then
|
||||
gained = gained + gem.xp;
|
||||
gem.actor:Hide();
|
||||
tinsert(gemPool, gem);
|
||||
table.remove(gems, i);
|
||||
else
|
||||
-- It flies at you, faster than you run, and rises to your chest.
|
||||
local d = math.sqrt(d2);
|
||||
local step = math.min(1, GEM_SPEED * dt / d);
|
||||
gem.x = gem.x + (hx - gem.x) * step;
|
||||
gem.y = gem.y + (hy - gem.y) * step;
|
||||
gem.z = gem.z + (hz + 1 - gem.z) * step;
|
||||
gem.actor:SetPosition(gem.x, gem.y, gem.z);
|
||||
end
|
||||
end
|
||||
end
|
||||
if gained > 0 then
|
||||
GainXp(gained * (1 + PASSIVES.wisdom.per * (BW.run.passives.wisdom or 0) / 100));
|
||||
end
|
||||
|
||||
-- Spells.
|
||||
local haste = 1 - 0.08 * (BW.run.passives.haste or 0);
|
||||
for key, state in pairs(BW.run.spells) do
|
||||
state.cooldown = state.cooldown - dt;
|
||||
if state.cooldown <= 0 and state.pending == 0 and Fire(key, state) then
|
||||
state.cooldown = BW.AfterCast(Stat(SPELLS[key], "cooldown", state.level) * haste); -- Gear.lua
|
||||
state.cdFull = state.cooldown;
|
||||
if state.slot then
|
||||
state.slot.cd:SetCooldown(GetTime(), state.cooldown);
|
||||
end
|
||||
end
|
||||
end
|
||||
BW.run.castGap = BW.run.castGap - dt;
|
||||
if BW.run.castGap <= 0 then
|
||||
CastNext();
|
||||
end
|
||||
|
||||
if BW.run.time >= RUN_SECONDS then
|
||||
EndRun(true);
|
||||
end
|
||||
end
|
||||
|
||||
local function UpdateHud(dt, luaMs)
|
||||
local second = math.floor(BW.run.time);
|
||||
if second ~= BW.run.shownSecond then
|
||||
BW.run.shownSecond = second;
|
||||
BW.infoText:SetText(string.format("%s Kills %d", Clock(BW.run.time), BW.run.kills));
|
||||
end
|
||||
BW.run.perfFrames = BW.run.perfFrames + 1;
|
||||
BW.run.perfLua = BW.run.perfLua + luaMs;
|
||||
BW.run.perfClock = BW.run.perfClock + dt;
|
||||
if BW.run.perfClock >= 1 then
|
||||
BW.perfText:SetText(string.format("%.0f fps Lua %.2f ms/frame %d alive %d gems",
|
||||
BW.run.perfFrames / BW.run.perfClock, BW.run.perfLua / BW.run.perfFrames, BW.run.alive, table.getn(gems)));
|
||||
BW.run.perfFrames, BW.run.perfLua, BW.run.perfClock = 0, 0, 0;
|
||||
end
|
||||
end
|
||||
|
||||
local function OnUpdate()
|
||||
if not BW.run or BW.paused or BW.run.over then
|
||||
return;
|
||||
end
|
||||
local dt = arg1;
|
||||
local t0 = debugprofilestop();
|
||||
Steer();
|
||||
Step(dt);
|
||||
UpdateNumbers(dt);
|
||||
local luaMs = debugprofilestop() - t0;
|
||||
if not BW.run.over then
|
||||
UpdateHud(dt, luaMs);
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Keys
|
||||
|
||||
local MOVE_KEYS = { W = true, A = true, S = true, D = true }
|
||||
|
||||
local function OnKeyDown()
|
||||
local key = arg1;
|
||||
if key == "ESCAPE" then
|
||||
BW.frame:Hide();
|
||||
return;
|
||||
end
|
||||
if not BW.run then
|
||||
return;
|
||||
end
|
||||
if BW.run.over then
|
||||
if key == "R" then
|
||||
StartRun();
|
||||
end
|
||||
return;
|
||||
end
|
||||
if BW.cards:IsShown() then
|
||||
if key == "1" or key == "2" or key == "3" then
|
||||
Broodwatch_PickCard(tonumber(key));
|
||||
end
|
||||
return;
|
||||
end
|
||||
if key == "P" then
|
||||
SetPaused(not BW.paused);
|
||||
BW.bannerText:SetText(BW.paused and "Paused" or "");
|
||||
BW.bannerText:SetTextColor(NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b);
|
||||
BW.hintText:SetText(BW.paused and "P: resume." or "");
|
||||
return;
|
||||
end
|
||||
if MOVE_KEYS[key] then
|
||||
BW.held[key] = true;
|
||||
Steer();
|
||||
end
|
||||
end
|
||||
|
||||
local function OnKeyUp()
|
||||
BW.held[arg1] = nil;
|
||||
Steer();
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Setup
|
||||
|
||||
local function Build()
|
||||
debugprofilestart();
|
||||
BW.scene:TransitionToModelSceneID(4, CAMERA_TRANSITION_TYPE_IMMEDIATE, CAMERA_MODIFICATION_TYPE_DISCARD, true);
|
||||
BW.scene:SetWorldModelByMapID(LAIR_MAP);
|
||||
BW.scene:SetCameraFarClip(FAR_CLIP);
|
||||
|
||||
local _, cameraIDs = C_ModelInfo.GetModelSceneInfoByID(4);
|
||||
BW.camera = BW.scene:AddCamera(CameraRegistry:CreateCameraByType("FollowCamera"));
|
||||
BW.camera:ApplyFromModelSceneCameraInfo(C_ModelInfo.GetModelSceneCameraInfoByID(cameraIDs[1]),
|
||||
CAMERA_TRANSITION_TYPE_IMMEDIATE, CAMERA_MODIFICATION_TYPE_DISCARD);
|
||||
BW.camera:SetTargetSpline(nil);
|
||||
BW.camera:SetOrientationSpline(nil);
|
||||
BW.camera:SetMinZoomDistance(12);
|
||||
BW.camera:SetMaxZoomDistance(60);
|
||||
|
||||
BW.hero = BW.scene:AcquireActor();
|
||||
BW.hero:SetModelByUnit("player");
|
||||
BW.hero:HookScript("OnSpellVisualImpact", BW.OnImpact);
|
||||
BW.scene:SetSoundListenerActor(BW.hero);
|
||||
|
||||
BW.camera:SetFollowActor(BW.hero);
|
||||
BW.camera:SetYaw(CAMERA_YAW);
|
||||
BW.camera:SetPitch(CAMERA_PITCH);
|
||||
BW.camera:SetZoomDistance(CAMERA_ZOOM);
|
||||
BW.camera:SnapAllInterpolatedValues();
|
||||
BW.scene:SetActiveCamera(BW.camera);
|
||||
end
|
||||
|
||||
function Broodwatch_OnLoad(self)
|
||||
BW.frame = self;
|
||||
BW.scene = getglobal(self:GetName() .. "Scene");
|
||||
BW.feedback = getglobal(self:GetName() .. "Feedback");
|
||||
BW.hurtTexture = getglobal(self:GetName() .. "FeedbackHurt");
|
||||
BW.hurtTexture:SetAlpha(0);
|
||||
-- Siblings at one frame level draw in no set order, and the scene's
|
||||
-- paint covered the feedback (item icons, numbers). Chained parents
|
||||
-- order them: scene, feedback over it, the HUD over both. SetParent
|
||||
-- moves a frame's children with it; SetFrameLevel would not.
|
||||
local hud = self:GetName() .. "Hud";
|
||||
BW.feedback:SetParent(BW.scene);
|
||||
getglobal(hud):SetParent(BW.feedback);
|
||||
BW.infoText = getglobal(hud .. "Info");
|
||||
BW.perfText = getglobal(hud .. "Perf");
|
||||
BW.bannerText = getglobal(hud .. "Banner");
|
||||
BW.hintText = getglobal(hud .. "Hint");
|
||||
BW.xpBar = getglobal(hud .. "XP");
|
||||
BW.xpText = getglobal(hud .. "XPText");
|
||||
BW.healthBar = getglobal(hud .. "Health");
|
||||
BW.healthText = getglobal(hud .. "HealthText");
|
||||
BW.cards = getglobal(hud .. "Cards");
|
||||
BW.build = getglobal(hud .. "Build");
|
||||
self:SetScript("OnUpdate", OnUpdate);
|
||||
self:SetScript("OnKeyDown", OnKeyDown);
|
||||
self:SetScript("OnKeyUp", OnKeyUp);
|
||||
self:SetScript("OnShow", function()
|
||||
BW.frame:EnableKeyboard(true);
|
||||
end);
|
||||
self:SetScript("OnHide", function()
|
||||
BW.frame:EnableKeyboard(false);
|
||||
if BW.run and not BW.run.over then
|
||||
EndRun(false);
|
||||
end
|
||||
end);
|
||||
end
|
||||
|
||||
SLASH_BROODWATCH1 = "/broodwatch";
|
||||
-- `/broodwatch [spell [rank] ...]`: spell keys of SPELLS (each with its
|
||||
-- rank, I by default) start the run with those spells alone, for testing
|
||||
-- them: `/broodwatch charge 5 whirlwind 5` makes Intercept the next pick.
|
||||
-- `/broodwatch chest` (or `golden`) starts the run with a chest close by,
|
||||
-- `/broodwatch eggs` with a clutch of eggs, `/broodwatch vents` with the
|
||||
-- lava vents erupting from the start, `/broodwatch <kind>` (a key of
|
||||
-- KINDS, or `elite`) with three of that kind, and `/broodwatch <gear> ...`
|
||||
-- (keys of GEAR) wearing that gear.
|
||||
SlashCmdList["BROODWATCH"] = function(msg)
|
||||
BW.frame:Show();
|
||||
if not BW.hero then
|
||||
Build();
|
||||
end
|
||||
local _, _, key = string.find(msg or "", "^%s*(%w+)");
|
||||
if key and SPELLS[key] then
|
||||
local spells = {};
|
||||
for spell, rank in string.gfind(msg, "(%a+)%s*(%d*)") do
|
||||
if SPELLS[spell] then
|
||||
spells[spell] = { level = math.min(tonumber(rank) or 1, MAX_LEVEL), cooldown = 0, pending = 0 };
|
||||
else
|
||||
print("Broodwatch: no spell \"" .. spell .. "\"");
|
||||
end
|
||||
end
|
||||
StartRun(spells);
|
||||
return;
|
||||
end
|
||||
StartRun();
|
||||
if not key then
|
||||
return;
|
||||
elseif key == "chest" or key == "golden" then
|
||||
if key == "golden" then
|
||||
BW.run.chests = BW.GOLDEN_EVERY - 1;
|
||||
end
|
||||
local hx, hy, hz = BW.hero:GetPosition();
|
||||
BW.DropChest(hx + 8, hy, hz);
|
||||
elseif key == "eggs" then
|
||||
BW.LayClutch();
|
||||
elseif key == "vents" then
|
||||
BW.run.nextVents = 0;
|
||||
elseif key and BW.GEAR[key] then
|
||||
for gear in string.gfind(msg, "(%a+)") do
|
||||
if BW.GEAR[gear] then
|
||||
BW.Equip(gear);
|
||||
else
|
||||
print("Broodwatch: no gear \"" .. gear .. "\"");
|
||||
end
|
||||
end
|
||||
BW.UpdateBuild();
|
||||
elseif key and BW.KindByKey(key) then
|
||||
for _ = 1, 3 do
|
||||
Spawn(BW.KindByKey(key));
|
||||
end
|
||||
elseif key then
|
||||
print("Broodwatch: no spell \"" .. key .. "\"");
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,20 @@
|
||||
## Interface: 11200
|
||||
## Title: Broodwatch
|
||||
## Notes: Hold Onyxia's Lair against an endless brood. /broodwatch starts a run.
|
||||
## Dependencies: !!!ClassicAPI
|
||||
Data.lua
|
||||
Core.lua
|
||||
Brood.lua
|
||||
Eggs.lua
|
||||
Mines.lua
|
||||
Spells.lua
|
||||
Kinds.lua
|
||||
Hud.lua
|
||||
Items.lua
|
||||
Chests.lua
|
||||
Vents.lua
|
||||
Foes.lua
|
||||
Gear.lua
|
||||
Markers.lua
|
||||
Broodwatch.lua
|
||||
Broodwatch.xml
|
||||
@@ -0,0 +1,238 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
..\FrameXML\UI.xsd">
|
||||
<!-- A level-up card: icon, name, what kind of pick it is, what it changes,
|
||||
five rank pips and the key that picks it. Broodwatch.lua fills it. -->
|
||||
<Button name="BroodwatchCardTemplate" virtual="true">
|
||||
<Size x="210" y="280"/>
|
||||
<Backdrop bgFile="Interface\Tooltips\UI-Tooltip-Background" edgeFile="Interface\Tooltips\UI-Tooltip-Border" tile="true">
|
||||
<EdgeSize>
|
||||
<AbsValue val="16"/>
|
||||
</EdgeSize>
|
||||
<TileSize>
|
||||
<AbsValue val="16"/>
|
||||
</TileSize>
|
||||
<BackgroundInsets>
|
||||
<AbsInset left="4" right="4" top="4" bottom="4"/>
|
||||
</BackgroundInsets>
|
||||
</Backdrop>
|
||||
<Layers>
|
||||
<Layer level="ARTWORK">
|
||||
<Texture name="$parentIcon">
|
||||
<Size x="64" y="64"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOP" x="0" y="-20"/>
|
||||
</Anchors>
|
||||
</Texture>
|
||||
<Texture name="$parentPip3">
|
||||
<Size x="18" y="6"/>
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOM" x="0" y="40"/>
|
||||
</Anchors>
|
||||
<Color r="1" g="1" b="1" a="1"/>
|
||||
</Texture>
|
||||
<Texture name="$parentPip2">
|
||||
<Size x="18" y="6"/>
|
||||
<Anchors>
|
||||
<Anchor point="RIGHT" relativeTo="$parentPip3" relativePoint="LEFT" x="-4" y="0"/>
|
||||
</Anchors>
|
||||
<Color r="1" g="1" b="1" a="1"/>
|
||||
</Texture>
|
||||
<Texture name="$parentPip1">
|
||||
<Size x="18" y="6"/>
|
||||
<Anchors>
|
||||
<Anchor point="RIGHT" relativeTo="$parentPip2" relativePoint="LEFT" x="-4" y="0"/>
|
||||
</Anchors>
|
||||
<Color r="1" g="1" b="1" a="1"/>
|
||||
</Texture>
|
||||
<Texture name="$parentPip4">
|
||||
<Size x="18" y="6"/>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT" relativeTo="$parentPip3" relativePoint="RIGHT" x="4" y="0"/>
|
||||
</Anchors>
|
||||
<Color r="1" g="1" b="1" a="1"/>
|
||||
</Texture>
|
||||
<Texture name="$parentPip5">
|
||||
<Size x="18" y="6"/>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT" relativeTo="$parentPip4" relativePoint="RIGHT" x="4" y="0"/>
|
||||
</Anchors>
|
||||
<Color r="1" g="1" b="1" a="1"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
<Layer level="OVERLAY">
|
||||
<!-- As wide as the description: a long name wraps (Hud.lua's FillCard measures it). -->
|
||||
<FontString name="$parentName" inherits="GameFontNormalLarge" justifyH="CENTER" justifyV="TOP">
|
||||
<Size x="184" y="20"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOP" relativeTo="$parentIcon" relativePoint="BOTTOM" x="0" y="-10"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
<FontString name="$parentTag" inherits="GameFontHighlightSmall">
|
||||
<Anchors>
|
||||
<Anchor point="TOP" relativeTo="$parentName" relativePoint="BOTTOM" x="0" y="-4"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
<FontString name="$parentDesc" inherits="GameFontHighlight" justifyH="CENTER" justifyV="TOP">
|
||||
<Size x="184" y="96"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOP" relativeTo="$parentTag" relativePoint="BOTTOM" x="0" y="-14"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
<FontString name="$parentKey" inherits="GameFontDisableSmall">
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOM" x="0" y="16"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnClick>
|
||||
Broodwatch_PickCard(this:GetID());
|
||||
</OnClick>
|
||||
<OnEnter>
|
||||
Broodwatch_CardHover(this, true);
|
||||
</OnEnter>
|
||||
<OnLeave>
|
||||
Broodwatch_CardHover(this, false);
|
||||
</OnLeave>
|
||||
</Scripts>
|
||||
</Button>
|
||||
|
||||
<Frame name="BroodwatchFrame" parent="UIParent" toplevel="true" hidden="true" frameStrata="DIALOG">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" x="40" y="-40"/>
|
||||
<Anchor point="BOTTOMRIGHT" x="-40" y="40"/>
|
||||
</Anchors>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture setAllPoints="true">
|
||||
<Color r="0" g="0" b="0" a="1"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<ModelScene name="$parentScene" inherits="ModelSceneMixinTemplate" setAllPoints="true"/>
|
||||
<Frame name="$parentFeedback" setAllPoints="true">
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture name="$parentHurt" file="Interface\FullScreenTextures\LowHealth" alphaMode="ADD" setAllPoints="true"/>
|
||||
</Layer>
|
||||
</Layers>
|
||||
</Frame>
|
||||
<Frame name="$parentHud" setAllPoints="true">
|
||||
<Layers>
|
||||
<Layer level="OVERLAY">
|
||||
<FontString name="$parentInfo" inherits="GameFontHighlightLarge" justifyH="LEFT">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" x="14" y="-14"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
<FontString name="$parentPerf" inherits="GameFontHighlightSmall" justifyH="RIGHT">
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOMRIGHT" x="-10" y="10"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
<FontString name="$parentBanner" inherits="GameFontNormalHuge">
|
||||
<Anchors>
|
||||
<Anchor point="CENTER" x="0" y="140"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
<FontString name="$parentHint" inherits="GameFontHighlight">
|
||||
<Anchors>
|
||||
<Anchor point="TOP" relativeTo="$parentBanner" relativePoint="BOTTOM" x="0" y="-10"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<!-- Experience: on the health bar, as wide, with its level and what is left. -->
|
||||
<StatusBar name="$parentXP">
|
||||
<Size x="320" y="14"/>
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOM" x="0" y="44"/>
|
||||
</Anchors>
|
||||
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
|
||||
<BarColor r="0.58" g="0.0" b="0.55"/>
|
||||
<Layers>
|
||||
<Layer level="OVERLAY">
|
||||
<FontString name="$parentText" inherits="GameFontHighlightSmall">
|
||||
<Anchors>
|
||||
<Anchor point="CENTER"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
</StatusBar>
|
||||
<StatusBar name="$parentHealth">
|
||||
<Size x="320" y="16"/>
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOM" x="0" y="24"/>
|
||||
</Anchors>
|
||||
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
|
||||
<BarColor r="0.1" g="0.85" b="0.1"/>
|
||||
<Layers>
|
||||
<Layer level="OVERLAY">
|
||||
<FontString name="$parentText" inherits="GameFontHighlightSmall">
|
||||
<Anchors>
|
||||
<Anchor point="CENTER"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
</StatusBar>
|
||||
<!-- What you have: one slot per spell and passive, filled by Lua. -->
|
||||
<Frame name="$parentBuild">
|
||||
<Size x="10" y="34"/>
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOMLEFT" x="12" y="12"/>
|
||||
</Anchors>
|
||||
</Frame>
|
||||
<!-- The level-up screen: the scene dims behind three cards. -->
|
||||
<Frame name="$parentCards" hidden="true" setAllPoints="true">
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture setAllPoints="true">
|
||||
<Color r="0" g="0" b="0" a="0.55"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
<Layer level="OVERLAY">
|
||||
<FontString name="$parentTitle" inherits="GameFontNormalHuge">
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOM" relativePoint="CENTER" x="0" y="160"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
<FontString name="$parentHint" inherits="GameFontHighlight" text="Choose an upgrade: click a card, or press 1, 2 or 3.">
|
||||
<Anchors>
|
||||
<Anchor point="TOP" relativeTo="$parentTitle" relativePoint="BOTTOM" x="0" y="-6"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<Button name="$parentCard2" inherits="BroodwatchCardTemplate" id="2">
|
||||
<Anchors>
|
||||
<Anchor point="TOP" relativeTo="$parentHint" relativePoint="BOTTOM" x="0" y="-24"/>
|
||||
</Anchors>
|
||||
</Button>
|
||||
<Button name="$parentCard1" inherits="BroodwatchCardTemplate" id="1">
|
||||
<Anchors>
|
||||
<Anchor point="RIGHT" relativeTo="$parentCard2" relativePoint="LEFT" x="-18" y="0"/>
|
||||
</Anchors>
|
||||
</Button>
|
||||
<Button name="$parentCard3" inherits="BroodwatchCardTemplate" id="3">
|
||||
<Anchors>
|
||||
<Anchor point="LEFT" relativeTo="$parentCard2" relativePoint="RIGHT" x="18" y="0"/>
|
||||
</Anchors>
|
||||
</Button>
|
||||
</Frames>
|
||||
</Frame>
|
||||
</Frames>
|
||||
</Frame>
|
||||
</Frames>
|
||||
<Scripts>
|
||||
<OnLoad>
|
||||
Broodwatch_OnLoad(this);
|
||||
</OnLoad>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
</Ui>
|
||||
@@ -0,0 +1,107 @@
|
||||
-- Broodwatch chests: an elite's reward, opened by walking up to it.
|
||||
|
||||
local _, BW = ...
|
||||
local CHESTS = BW.CHESTS
|
||||
local CHEST_SOUND = BW.CHEST_SOUND
|
||||
local GOLDEN_EVERY = BW.GOLDEN_EVERY
|
||||
local TUNING = BW.TUNING
|
||||
local Dist2 = BW.Dist2
|
||||
local FloorAt = BW.FloorAt
|
||||
local ShowCards = BW.ShowCards
|
||||
|
||||
local ANIM_CLOSED = 147
|
||||
local ANIM_OPEN = 148
|
||||
local ANIM_OPENED = 149
|
||||
|
||||
local chests = {}
|
||||
local chestPool = {}
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- A chest lies closed (Closed 147) under a reticle until the hero comes
|
||||
-- within CHEST_REACH. Then it opens (Open 148), and when the lid is up it
|
||||
-- holds Opened (149), gives its picks, stays CHEST_LINGER seconds and fades.
|
||||
-- The open runs on the scene's clock, so a pause holds it.
|
||||
|
||||
BW.DropChest = function(x, y, z)
|
||||
BW.run.chests = BW.run.chests + 1;
|
||||
local kind = math.mod(BW.run.chests, GOLDEN_EVERY) == 0 and CHESTS.golden or CHESTS.plain;
|
||||
local chest = table.remove(chestPool);
|
||||
if not chest then
|
||||
chest = { actor = BW.scene:AcquireActor() };
|
||||
chest.actor:SetAnimationBlendOperation(Enum.ModelBlendOperation.None);
|
||||
end
|
||||
if chest.kind ~= kind then
|
||||
chest.actor:SetModelByPath(kind.path);
|
||||
chest.actor:SetScale(kind.scale);
|
||||
end
|
||||
chest.kind = kind;
|
||||
chest.state = "closed";
|
||||
-- It lands a little way from the corpse, toward the hero, and faces the hero.
|
||||
local hx, hy = BW.hero:GetPosition();
|
||||
local angle = math.atan2(hy - y, hx - x);
|
||||
chest.x = x + TUNING.CHEST_AWAY * math.cos(angle);
|
||||
chest.y = y + TUNING.CHEST_AWAY * math.sin(angle);
|
||||
chest.z = FloorAt(chest.x, chest.y, z);
|
||||
chest.actor:SetPosition(chest.x, chest.y, chest.z);
|
||||
chest.actor:SetYaw(angle);
|
||||
chest.actor:SetAnimation(ANIM_CLOSED);
|
||||
chest.actor:SetGroundReticle(TUNING.CHEST_REACH);
|
||||
chest.actor:Show();
|
||||
tinsert(chests, chest);
|
||||
end
|
||||
|
||||
local function ReleaseChest(i)
|
||||
local chest = chests[i];
|
||||
chest.actor:ClearGroundReticle();
|
||||
chest.actor:Hide();
|
||||
tinsert(chestPool, chest);
|
||||
table.remove(chests, i);
|
||||
end
|
||||
|
||||
local function UpdateChests(dt, hx, hy)
|
||||
local reach2 = TUNING.CHEST_REACH * TUNING.CHEST_REACH;
|
||||
for i = table.getn(chests), 1, -1 do
|
||||
local chest = chests[i];
|
||||
if chest.state == "closed" then
|
||||
if Dist2(chest.x, chest.y, hx, hy) <= reach2 then
|
||||
chest.state = "opening";
|
||||
chest.left = chest.kind.open / chest.kind.speed;
|
||||
chest.actor:ClearGroundReticle();
|
||||
chest.actor:SetAnimation(ANIM_OPEN, nil, chest.kind.speed);
|
||||
chest.actor:PlaySound(CHEST_SOUND);
|
||||
end
|
||||
else
|
||||
chest.left = chest.left - dt;
|
||||
if chest.left <= 0 then
|
||||
if chest.state == "opening" then
|
||||
chest.state = "open";
|
||||
chest.left = TUNING.CHEST_LINGER;
|
||||
chest.actor:SetAnimation(ANIM_OPENED);
|
||||
BW.run.chestTitle = chest.kind.title;
|
||||
-- A golden chest's first pick is a weapon, while you hold none.
|
||||
if chest.kind == CHESTS.golden and not BW.run.weapon then
|
||||
BW.run.weaponPick = true;
|
||||
end
|
||||
BW.run.chestPicks = BW.run.chestPicks + chest.kind.picks;
|
||||
ShowCards();
|
||||
elseif chest.state == "open" then
|
||||
chest.state = "fading";
|
||||
chest.actor:FadeOut();
|
||||
elseif not chest.actor:IsShown() then
|
||||
ReleaseChest(i);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function ClearChests()
|
||||
for i = table.getn(chests), 1, -1 do
|
||||
ReleaseChest(i);
|
||||
end
|
||||
end
|
||||
|
||||
-- For the files that load after this one.
|
||||
BW.chests = chests
|
||||
BW.UpdateChests = UpdateChests
|
||||
BW.ClearChests = ClearChests
|
||||
@@ -0,0 +1,191 @@
|
||||
-- Broodwatch core: the shared state, small helpers and the gems.
|
||||
--
|
||||
-- The files share the addon table BW (`local _, BW = ...`). A file keeps
|
||||
-- its own helpers local. What another file calls, and state that changes,
|
||||
-- lives on BW; constants are copied into a local at the top of each file
|
||||
-- that reads them. The TOC loads Data first and Broodwatch.lua last.
|
||||
|
||||
local _, BW = ...
|
||||
local GEM_PATH = BW.GEM_PATH
|
||||
local MAX_GEMS = BW.MAX_GEMS
|
||||
local MAX_LEVEL = BW.MAX_LEVEL
|
||||
local GEAR = BW.GEAR
|
||||
|
||||
-- Whether the hero has the spell at rank V (its mastery).
|
||||
local function Mastered(key)
|
||||
local state = BW.run and BW.run.spells[key];
|
||||
return state ~= nil and state.level >= MAX_LEVEL;
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- State
|
||||
|
||||
-- BW.OnImpact (the impact of any cast in flight) is defined with the spells,
|
||||
-- BW.DropItem (a kill's chance of an item) with the items.
|
||||
|
||||
BW.held = {}
|
||||
BW.moving = false
|
||||
|
||||
local pools = {} -- kind key -> list of free enemy records
|
||||
local enemies = {} -- live and dying enemy records
|
||||
local gemPool = {}
|
||||
local gems = {}
|
||||
|
||||
-- BW.run is the current run, or nil.
|
||||
BW.paused = false
|
||||
local offers = {} -- the three level-up choices
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Helpers
|
||||
|
||||
local function FloorAt(x, y, z)
|
||||
local _, _, hz = BW.scene:TraceSegment(x, y, z + 10, x, y, z - 50);
|
||||
return hz or z;
|
||||
end
|
||||
|
||||
local function Dist2(ax, ay, bx, by)
|
||||
local dx, dy = ax - bx, ay - by;
|
||||
return dx * dx + dy * dy;
|
||||
end
|
||||
|
||||
local function Clock(seconds)
|
||||
local s = math.floor(seconds);
|
||||
return string.format("%d:%02d", math.floor(s / 60), math.mod(s, 60));
|
||||
end
|
||||
|
||||
-- Damage that is not a spell's (items, mines) grows as the brood's health
|
||||
-- does (KindStats: (1 + m)^3, m the run's minutes over 5).
|
||||
local function RunGrowth()
|
||||
local m = 1 + BW.run.time / 300;
|
||||
return m * m * m;
|
||||
end
|
||||
|
||||
local function Shuffle(list)
|
||||
for i = table.getn(list), 2, -1 do
|
||||
local j = math.random(i);
|
||||
list[i], list[j] = list[j], list[i];
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- The hero's movement: forward or back (1, -1 or 0), to the side (1 left,
|
||||
-- -1 right, 0) and the facing (nil keeps it), sent to the actor only when
|
||||
-- they change, since each command re-anchors the move. Back is the client's
|
||||
-- backpedal, slower than a run; the side is its strafe.
|
||||
|
||||
BW.moveFwd, BW.moveSide, BW.moveFacing = 0, 0, nil
|
||||
|
||||
local function Drive(fwd, side, facing)
|
||||
local hero = BW.hero;
|
||||
if facing and facing ~= BW.moveFacing then
|
||||
BW.moveFacing = facing;
|
||||
hero:SetYaw(facing);
|
||||
end
|
||||
if fwd ~= BW.moveFwd then
|
||||
BW.moveFwd = fwd;
|
||||
if fwd > 0 then
|
||||
hero:StartMovingForward();
|
||||
elseif fwd < 0 then
|
||||
hero:StartMovingBackward();
|
||||
else
|
||||
hero:StopMoving();
|
||||
end
|
||||
end
|
||||
if side ~= BW.moveSide then
|
||||
BW.moveSide = side;
|
||||
if side > 0 then
|
||||
hero:StartStrafingLeft();
|
||||
elseif side < 0 then
|
||||
hero:StartStrafingRight();
|
||||
else
|
||||
hero:StopStrafing();
|
||||
end
|
||||
end
|
||||
BW.moving = fwd ~= 0 or side ~= 0;
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- The hero is hurt by `amount` of `school` ("Physical" for the brood's bites
|
||||
-- and charges). The step takes it from your health: a shield soaks it, the
|
||||
-- grace after the cards drops it. `breath`: a dragon's breath. What you wear
|
||||
-- and drink softens it first: Onyxia Scale Cloak stops breath, a Limited
|
||||
-- Invulnerability Potion stops blows, Drake Fang Talisman dodges a share of
|
||||
-- them, Drakefire Amulet halves fire, and a Fire Protection Potion's ward
|
||||
-- soaks fire.
|
||||
|
||||
local function HurtHero(amount, school, breath)
|
||||
local run = BW.run;
|
||||
if breath and run.gear.onyxiacloak then
|
||||
return;
|
||||
end
|
||||
if school == "Physical" then
|
||||
if run.invulnUntil then
|
||||
return;
|
||||
end
|
||||
if run.gear.drakefang then
|
||||
amount = amount * (1 - GEAR.drakefang.share);
|
||||
end
|
||||
elseif school == "Fire" then
|
||||
if run.gear.drakefire then
|
||||
amount = amount * (1 - GEAR.drakefire.share);
|
||||
end
|
||||
if run.fireWard then
|
||||
local soak = math.min(run.fireWard, amount);
|
||||
run.fireWard = run.fireWard - soak;
|
||||
amount = amount - soak;
|
||||
if run.fireWard <= 0 then
|
||||
run.fireWard = nil;
|
||||
end
|
||||
BW.UpdateHealth();
|
||||
end
|
||||
end
|
||||
run.pendingHurt = run.pendingHurt + amount;
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Gems
|
||||
|
||||
local function DropGem(x, y, z, xp, scale)
|
||||
if table.getn(gems) >= MAX_GEMS then
|
||||
-- Too many on the floor: the experience goes straight to the oldest gem.
|
||||
gems[1].xp = gems[1].xp + xp;
|
||||
return;
|
||||
end
|
||||
local gem = table.remove(gemPool);
|
||||
if not gem then
|
||||
gem = { actor = BW.scene:AcquireActor() };
|
||||
gem.actor:SetModelByPath(GEM_PATH);
|
||||
end
|
||||
gem.xp = xp;
|
||||
gem.x, gem.y, gem.z = x, y, z;
|
||||
gem.pulled = false;
|
||||
gem.actor:SetScale(scale or 0.3);
|
||||
gem.actor:SetPosition(x, y, z);
|
||||
gem.actor:Show();
|
||||
tinsert(gems, gem);
|
||||
end
|
||||
|
||||
local function ClearGems()
|
||||
for i = table.getn(gems), 1, -1 do
|
||||
gems[i].actor:Hide();
|
||||
tinsert(gemPool, gems[i]);
|
||||
gems[i] = nil;
|
||||
end
|
||||
end
|
||||
|
||||
-- For the files that load after this one.
|
||||
BW.Mastered = Mastered
|
||||
BW.pools = pools
|
||||
BW.enemies = enemies
|
||||
BW.gemPool = gemPool
|
||||
BW.gems = gems
|
||||
BW.offers = offers
|
||||
BW.FloorAt = FloorAt
|
||||
BW.Dist2 = Dist2
|
||||
BW.Clock = Clock
|
||||
BW.Shuffle = Shuffle
|
||||
BW.Drive = Drive
|
||||
BW.HurtHero = HurtHero
|
||||
BW.RunGrowth = RunGrowth
|
||||
BW.DropGem = DropGem
|
||||
BW.ClearGems = ClearGems
|
||||
@@ -0,0 +1,919 @@
|
||||
-- Broodwatch tuning and data: the game's constants, spells, passives and items.
|
||||
|
||||
local _, BW = ...
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Tuning
|
||||
|
||||
local LAIR_MAP = 249
|
||||
local CENTER = { 0.7, -211.97, -86.08 } -- Onyxia's spawn point, the room's floor
|
||||
local ROOM_RADIUS = 22 -- yards around CENTER where the brood spawns
|
||||
local SPAWN_MIN_DISTANCE = 13 -- yards from the hero
|
||||
local FAR_CLIP = 500
|
||||
|
||||
local CAMERA_YAW = 3.09
|
||||
local CAMERA_PITCH = 1.0
|
||||
local CAMERA_ZOOM = 50 -- each run starts zoomed out; the wheel zooms in
|
||||
|
||||
local RUN_SECONDS = 600
|
||||
local ELITE_EVERY = 90
|
||||
local MAX_ALIVE = 200
|
||||
local RETARGET_SECONDS = 0.3 -- each chaser gets a new MoveTo this often
|
||||
local SURROUND_RADIUS = 1.0 -- chasers aim at a ring around the hero, not its centre
|
||||
local CAST_SPACING = 0.3 -- seconds between two of the hero's casts
|
||||
local DEATH_SECONDS = 4.5 -- the death animation (1.5 s), then the corpse lies; then it fades out
|
||||
local ANIM_STAND = 0
|
||||
local ANIM_DEATH = 1
|
||||
local ANIM_ATTACK_UNARMED = 16
|
||||
|
||||
local HERO_HP = 100
|
||||
local MAGNET = 5 -- gems this close (yards) fly to you
|
||||
local GEM_SPEED = 20 -- yards a second, once pulled
|
||||
local GEM_TAKE = 0.8 -- a pulled gem this close is taken
|
||||
local GEM_PATH = "World\\Generic\\ActiveDoodads\\Rocks\\GemRock01.m2"
|
||||
local MAX_GEMS = 150
|
||||
|
||||
-- The brood. `from` is the second of the run when the kind starts to spawn.
|
||||
local KINDS = {
|
||||
{ key = "black", display = 497, hp = 20, dps = 5, xp = 1, reach = 1.4, speed = 3.0, from = 0 },
|
||||
-- Red whelps breathe fire: Flame Breath 16396, whose flame hangs where a
|
||||
-- whelp's own fireball leaves (its cast kit's sixth slot).
|
||||
{ key = "red", display = 956, hp = 30, dps = 6, xp = 2, reach = 1.4, speed = 4.4, from = 120,
|
||||
act = { kind = "breath", spell = 16396, school = "Fire", every = 7, range = 4.5, near = 2, cast = 0.9, recover = 0.5,
|
||||
length = 6, arc = math.pi / 3, hit = 1.5 } },
|
||||
{ key = "green", display = 694, hp = 70, dps = 9, xp = 3, reach = 1.4, speed = 2.4, from = 240 },
|
||||
-- Onyxian Warders (creature 12129's display) keep their distance and
|
||||
-- throw Fireball (133) at where you stand.
|
||||
{ key = "warder", display = 8711, hp = 120, dps = 8, xp = 8, reach = 2.6, speed = 3.2, from = 180, max = 5,
|
||||
keep = 13, gemScale = 0.5, knockShare = 0.6,
|
||||
act = { kind = "bolt", spell = 133, school = "Fire", every = 5, range = 20, cast = 1.2, recover = 0.6, radius = 2.5, hit = 2 } },
|
||||
-- Onyxian Drakes (creature 12260's display) charge along a line through
|
||||
-- you, with Charge's trail (100).
|
||||
{ key = "drake", display = 6369, hp = 260, dps = 12, xp = 12, reach = 3, speed = 4.0, from = 300, max = 3,
|
||||
gemScale = 0.6, holdShare = 0.5, knockShare = 0.3,
|
||||
act = { kind = "charge", spell = 100, school = "Physical", every = 8, range = 18, near = 4, cast = 1.2, recover = 1.2,
|
||||
speed = 24, past = 6, width = 1.6, hit = 2.5, knock = { 9, 6 } } },
|
||||
}
|
||||
-- `speed` is the run speed in yards a second (the hero runs at 7): black
|
||||
-- whelps plod, red ones dart, green ones are slow and tough, and the elite
|
||||
-- is faster than any whelp but still slower than you.
|
||||
-- A kind's traits are its fields, read by what they govern:
|
||||
-- - `holdShare`: the share of a root's, stun's, fear's or polymorph's time
|
||||
-- it takes (1 if absent); 0 is immune, and Polymorph never picks it.
|
||||
-- - `knockShare`: the share of a knockback it takes (1 if absent); 0 stands.
|
||||
-- - `speed` 0: it never moves. `dps` 0: it never bites.
|
||||
-- - `path`: a model file, in place of the creature `display`.
|
||||
-- - `max`: the most of the kind alive at once.
|
||||
-- - `keep`: it holds this many yards from you instead of closing in.
|
||||
-- - `act`: what it does besides bite (Foes.lua). It acts every `every`
|
||||
-- seconds while you are within `range` (and no closer than `near`): it
|
||||
-- stands for `cast` seconds with the floor marked where it will strike,
|
||||
-- strikes, then stands for `recover`. A strike that lands on you hurts for
|
||||
-- `hit` seconds of its bite, of `school`. `kind`: "bolt" (`spell` thrown at your spot,
|
||||
-- `radius` wide), "breath" (a cone `length` long and `arc` wide) or
|
||||
-- "charge" (a run at `speed` along a line `past` yards beyond you, `width`
|
||||
-- wide; it throws you by `knock`).
|
||||
-- - `crackAnim`, `crackSound`, `crackSeconds`: it ends with this animation
|
||||
-- and sound, holds the frame at `crackSeconds` (just short of the
|
||||
-- animation's end, before it loops) and fades, in place of Death and a
|
||||
-- corpse.
|
||||
local ELITE = { key = "elite", display = 2534, hp = 220, dps = 9, xp = 20, reach = 2.2, speed = 4.8, gemScale = 0.6,
|
||||
holdShare = 0.5, knockShare = 0.4 }
|
||||
-- A black dragon egg (Blackwing Lair's). Broken or hatched, it cracks with
|
||||
-- its gameobject's Custom0 (153) and that slot's sound (GameObjectDisplayInfo
|
||||
-- 477's sound 6, SoundEntries 4785).
|
||||
local EGG = { key = "egg", path = "World\\Goober\\G_DragonEggBlack.m2", hp = 60, dps = 0, xp = 6, reach = 0, speed = 0,
|
||||
gemScale = 0.5, holdShare = 0, knockShare = 0, crackAnim = 153, crackSound = 4785, crackSeconds = 0.95,
|
||||
icon = "Interface\\Icons\\INV_Egg_01" }
|
||||
|
||||
-- Spells, by level 1 to 5. A missile spell hits `count` enemies in `range`;
|
||||
-- a nova spell hits every enemy within `radius` of the hero. At rank V a
|
||||
-- spell masters: `mastery` is what it does then, and its numbers.
|
||||
local SPELLS = {
|
||||
frostbolt = {
|
||||
spell = 116, blurb = "Bolts the nearest enemy.", color = { 0.55, 0.8, 1 }, kind = "missile", pick = "nearest", range = 24,
|
||||
damage = { 14, 18, 18, 24, 26 }, count = { 1, 1, 2, 2, 3 }, cooldown = { 1.2, 1.05, 1.05, 0.9, 0.8 },
|
||||
mastery = { text = "Frostbite: each bolt freezes its target for 1 second.", freeze = 1 },
|
||||
},
|
||||
shadowbolt = {
|
||||
spell = 686, blurb = "A heavy bolt at the toughest enemy in range.", color = { 0.8, 0.45, 1 }, kind = "missile", pick = "strongest", range = 26,
|
||||
damage = { 45, 60, 75, 75, 100 }, count = { 1, 1, 1, 2, 2 }, cooldown = { 2.6, 2.4, 2.2, 2.2, 2.0 },
|
||||
mastery = { text = "Shadowburn: a bolt that kills bursts, hitting enemies within 4 yards for half its damage.", radius = 4, share = 0.5 },
|
||||
},
|
||||
lightning = {
|
||||
spell = 403, blurb = "Strikes enemies at random.", color = { 1, 0.95, 0.4 }, kind = "missile", pick = "random", range = 22,
|
||||
damage = { 22, 28, 34, 40, 48 }, count = { 1, 2, 2, 3, 4 }, cooldown = { 1.8, 1.7, 1.6, 1.5, 1.3 },
|
||||
mastery = { text = "Overload: each bolt has a 1 in 3 chance to strike another enemy.", chance = 1 / 3 },
|
||||
},
|
||||
nova = {
|
||||
spell = 1449, blurb = "Bursts arcane power at every enemy around you.", color = { 0.85, 0.55, 1 }, kind = "nova",
|
||||
damage = { 10, 14, 18, 22, 28 }, radius = { 6, 6.5, 7, 7.5, 8 }, cooldown = { 5, 4.6, 4.2, 3.8, 3.4 },
|
||||
mastery = { text = "Echo: each explosion bursts a second time.", echo = true },
|
||||
},
|
||||
chain = {
|
||||
spell = 421, blurb = "Leaps from enemy to enemy.",
|
||||
color = { 0.6, 0.75, 1 }, kind = "chain", pick = "nearest", range = 22, hop = 8,
|
||||
damage = { 20, 24, 30, 36, 44 }, jumps = { 3, 4, 4, 5, 6 }, cooldown = { 2.4, 2.2, 2.0, 1.8, 1.6 },
|
||||
mastery = { text = "Each leap strikes 20% harder than the one before.", grow = 1.2 },
|
||||
},
|
||||
corruption = {
|
||||
spell = 172, blurb = "Rots enemies every second, and spreads when they die.",
|
||||
color = { 0.55, 0.85, 0.3 }, kind = "dot", pick = "nearest", range = 22, hop = 6,
|
||||
damage = { 6, 8, 10, 12, 15 }, count = { 2, 3, 3, 4, 5 }, spread = { 1, 1, 2, 2, 3 },
|
||||
duration = { 6, 6, 7, 7, 8 }, cooldown = { 3, 3, 2.8, 2.6, 2.4 },
|
||||
mastery = { text = "Corrupted enemies take 25% more damage from everything.", amplify = 1.25 },
|
||||
},
|
||||
holynova = {
|
||||
spell = 15237, blurb = "Burns enemies around you, and heals you for each one.",
|
||||
color = { 1, 0.95, 0.6 }, kind = "nova",
|
||||
damage = { 12, 16, 20, 26, 32 }, radius = { 6, 6.5, 7, 7.5, 8 }, heal = { 1, 1, 1.5, 1.5, 2 },
|
||||
cooldown = { 4, 3.7, 3.4, 3.1, 2.8 },
|
||||
mastery = { text = "Enemies it burns are dazed: half speed for 2 seconds.", slow = 0.5, seconds = 2 },
|
||||
},
|
||||
-- A fire totem model (display 4589, FireElementalTotem.mdx) that pulses
|
||||
-- as the real Magma Totem does (its pulse spell 8187, whose visual puts an
|
||||
-- Immolate impact on each enemy it burns).
|
||||
totem = {
|
||||
spell = 8190, pulse = 8187, display = 4589,
|
||||
blurb = "Plants a totem that burns every enemy near it every 2 seconds.",
|
||||
color = { 1, 0.5, 0.15 }, kind = "totem", pulseEvery = 2,
|
||||
damage = { 14, 18, 23, 28, 35 }, radius = { 6, 6.5, 7, 7.5, 8 }, totems = { 1, 1, 2, 2, 3 },
|
||||
duration = { 12, 12, 14, 14, 16 }, cooldown = { 14, 13, 12, 11, 10 },
|
||||
mastery = { text = "When a totem's time runs out, it erupts for triple damage.", burst = 3 },
|
||||
},
|
||||
-- Ground areas: the spell's own area model (SetModelBySpellArea) stands
|
||||
-- where it lands and burns every enemy inside it each second. An area
|
||||
-- with `slow` also slows them to that share of their speed.
|
||||
blizzard = {
|
||||
spell = 10, blurb = "Ice storms down on the thickest pack and chills it.",
|
||||
color = { 0.55, 0.8, 1 }, kind = "area", place = "pack", range = 22,
|
||||
damage = { 8, 10, 13, 16, 20 }, radius = { 5, 5.5, 6, 6.5, 7 }, slow = { 0.7, 0.65, 0.6, 0.55, 0.5 },
|
||||
duration = { 4, 4, 5, 5, 6 },
|
||||
cooldown = { 6, 5.5, 5, 4.5, 4 },
|
||||
mastery = { text = "Deep Freeze: when the storm ends, it freezes everything inside for 2 seconds.", freeze = 2 },
|
||||
},
|
||||
rainoffire = {
|
||||
spell = 5740, blurb = "Fire rains on the thickest pack.",
|
||||
color = { 1, 0.45, 0.15 }, kind = "area", place = "pack", range = 22,
|
||||
damage = { 14, 18, 23, 28, 35 }, radius = { 4, 4.4, 4.8, 5.2, 6 }, duration = { 3, 3, 3, 4, 4 },
|
||||
cooldown = { 7, 6.5, 6, 5.5, 5 },
|
||||
mastery = { text = "A second storm falls on another enemy." },
|
||||
},
|
||||
consecration = {
|
||||
spell = 26573, blurb = "Hallows the ground at your feet.",
|
||||
color = { 1, 0.85, 0.4 }, kind = "area", place = "self",
|
||||
damage = { 6, 8, 10, 13, 16 }, radius = { 5, 5.5, 6, 6.5, 7 }, duration = { 6, 6, 7, 7, 8 },
|
||||
cooldown = { 7, 6.5, 6, 5.5, 5 },
|
||||
mastery = { text = "The hallowed ground moves with you." },
|
||||
},
|
||||
thunder = {
|
||||
spell = 6343, blurb = "Shakes the ground close around you.", color = { 1, 0.6, 0.2 }, kind = "nova",
|
||||
damage = { 16, 20, 26, 32, 40 }, radius = { 4, 4.4, 4.8, 5.2, 6 }, cooldown = { 3, 2.8, 2.6, 2.4, 2.2 },
|
||||
mastery = { text = "Aftershock: the clap throws the brood back.", knock = { 8, 5 } },
|
||||
},
|
||||
-- Tanky spells keep the brood off you. `kit` is a SpellVisualKit shown
|
||||
-- while the effect lasts: the shield's bubble, Frost Nova's ice on each
|
||||
-- frozen enemy, Psychic Scream's terror over each fleeing one.
|
||||
shield = {
|
||||
spell = 17, blurb = "A shield soaks up the brood's bites until it breaks.",
|
||||
color = { 1, 1, 0.75 }, kind = "shield",
|
||||
absorb = { 30, 40, 50, 60, 75 }, duration = { 10, 10, 12, 12, 15 }, cooldown = { 15, 14, 13, 12, 11 },
|
||||
mastery = { text = "When the shield breaks, it bursts and hurls the brood around you away.", radius = 6, damage = 30, knock = { 12, 8 } },
|
||||
},
|
||||
thorns = {
|
||||
spell = 467, blurb = "Every enemy biting you takes damage each second.",
|
||||
color = { 0.6, 0.9, 0.3 }, kind = "thorns",
|
||||
damage = { 6, 8, 10, 13, 16 }, duration = { 8, 9, 10, 11, 12 }, cooldown = { 14, 13, 12, 11, 10 },
|
||||
mastery = { text = "You heal 1 health for every biter it burns.", heal = 1 },
|
||||
},
|
||||
frostnova = {
|
||||
spell = 122, blurb = "Freezes every enemy around you in place.",
|
||||
color = { 0.6, 0.85, 1 }, kind = "nova",
|
||||
damage = { 6, 8, 10, 12, 15 }, radius = { 6, 6.5, 7, 7.5, 8 }, root = { 2, 2.5, 3, 3.5, 4 },
|
||||
cooldown = { 10, 9.5, 9, 8.5, 8 },
|
||||
mastery = { text = "Shatter: frozen enemies take double damage.", amplify = 2 },
|
||||
},
|
||||
scream = {
|
||||
spell = 8122, blurb = "Every enemy around you flees in terror.",
|
||||
color = { 0.75, 0.5, 1 }, kind = "nova",
|
||||
radius = { 6, 6.5, 7, 7.5, 8 }, fear = { 2, 2.5, 3, 3.5, 4 }, cooldown = { 14, 13, 12, 11, 10 },
|
||||
mastery = { text = "Terror: fleeing enemies take 10 damage a second.", damage = 10 },
|
||||
},
|
||||
demoshout = {
|
||||
spell = 1160, blurb = "Enemies around you bite softer for a while.",
|
||||
color = { 1, 0.55, 0.35 }, kind = "nova",
|
||||
radius = { 8, 8.5, 9, 9.5, 10 }, weaken = { 0.3, 0.35, 0.4, 0.45, 0.5 }, duration = { 6, 7, 8, 9, 10 },
|
||||
cooldown = { 10, 9.5, 9, 8.5, 8 },
|
||||
mastery = { text = "Battle cry: you run 20% faster while it lasts.", speed = 1.2 },
|
||||
},
|
||||
stoneclaw = {
|
||||
spell = 5730, display = 4588, blurb = "Plants a totem that the brood around it must attack.",
|
||||
color = { 0.8, 0.65, 0.4 }, kind = "ward",
|
||||
radius = { 6, 7, 8, 9, 10 }, duration = { 5, 6, 7, 8, 9 }, cooldown = { 16, 15, 14, 13, 12 },
|
||||
mastery = { text = "When the totem crumbles, it stuns the brood around it for 2 seconds.", stun = 2, aura = 20549 },
|
||||
},
|
||||
-- Fun ones. `knock` is the knockback (out, up), as the items' are.
|
||||
blastwave = {
|
||||
spell = 11113, blurb = "A ring of fire hurls the brood away from you.",
|
||||
color = { 1, 0.5, 0.2 }, kind = "nova", knock = { 10, 7 },
|
||||
damage = { 18, 24, 30, 38, 48 }, radius = { 6, 6.5, 7, 7.5, 8 }, cooldown = { 7, 6.6, 6.2, 5.8, 5.4 },
|
||||
mastery = { text = "The brood it hits is dazed: half speed for 3 seconds.", slow = 0.5, seconds = 3 },
|
||||
},
|
||||
-- Channels: the hero holds the spell's beam on one enemy (PlaySpellVisual's
|
||||
-- channel) and burns it every second for the duration.
|
||||
drainlife = {
|
||||
spell = 689, blurb = "A beam drains the toughest enemy in range, healing you every second.",
|
||||
color = { 0.55, 0.9, 0.4 }, kind = "channel", pick = "strongest", range = 20,
|
||||
damage = { 8, 10, 13, 16, 20 }, heal = { 2, 2.5, 3, 3.5, 4 }, duration = { 5, 5, 6, 6, 7 },
|
||||
cooldown = { 8, 7.5, 7, 6.5, 6 },
|
||||
mastery = { text = "When its target dies, the beam leaps to the next enemy.", leap = 8 },
|
||||
},
|
||||
mindflay = {
|
||||
spell = 15407, blurb = "A beam flays the toughest enemy in range and slows it.",
|
||||
color = { 0.75, 0.55, 1 }, kind = "channel", pick = "strongest", range = 22,
|
||||
damage = { 14, 18, 23, 28, 35 }, slow = { 0.5, 0.45, 0.4, 0.35, 0.3 }, duration = { 3, 3, 4, 4, 5 },
|
||||
cooldown = { 6, 5.5, 5, 4.5, 4 },
|
||||
mastery = { text = "Each tick also flays every enemy within 3 yards of its target.", radius = 3 },
|
||||
},
|
||||
-- The imp (display 4449) stays for the run and casts the real Firebolt.
|
||||
imp = {
|
||||
spell = 688, missile = 3110, display = 4449, blurb = "An imp follows you and hurls Firebolts.",
|
||||
color = { 1, 0.55, 0.2 }, kind = "pet", range = 22,
|
||||
damage = { 12, 15, 19, 24, 30 }, cooldown = { 1.6, 1.5, 1.4, 1.3, 1.2 },
|
||||
mastery = { text = "The imp hurls two Firebolts at once.", bolts = 2 },
|
||||
},
|
||||
-- Inferno (1122) plants its meteor at the thickest pack
|
||||
-- (PlaySpellVisualAtPoint). Where it lands, the brood is burned and
|
||||
-- stunned (Inferno Effect 22703: 2 s within 10 yards), and an Infernal
|
||||
-- (display 169) climbs out, rooted for a moment (22707: 1.7 s), then
|
||||
-- hunts, taunting the whelps it burns.
|
||||
inferno = {
|
||||
spell = 1122, display = 169, stunAura = 22703, -- Inferno Effect: its state kit 349 is StunSwirl
|
||||
blurb = "A meteor crashes into the thickest pack and an Infernal climbs out to fight.",
|
||||
color = { 1, 0.5, 0.15 }, kind = "inferno", range = 24,
|
||||
damage = { 40, 55, 70, 90, 115 }, radius = { 8, 8, 9, 9, 10 },
|
||||
hit = { 14, 18, 23, 29, 36 }, burn = { 3, 4, 5, 6, 8 }, duration = { 12, 13, 14, 15, 16 },
|
||||
cooldown = { 30, 28, 26, 24, 22 },
|
||||
mastery = { text = "Two meteors fall, the second on another part of the brood.", meteors = 2 },
|
||||
},
|
||||
-- Lightning Shield (324): its orbs circle you as the aura's state kit
|
||||
-- (AddAuraVisual, kit 292). An orb strikes a whelp that bites you, with
|
||||
-- the spell's impact kit 62 on it; the shield goes with its last orb.
|
||||
lshield = {
|
||||
spell = 324, zapKit = 62, blurb = "Orbs of lightning circle you and strike the whelps that bite you.",
|
||||
color = { 0.6, 0.75, 1 }, kind = "lshield",
|
||||
damage = { 18, 23, 29, 36, 45 }, orbs = { 3, 3, 4, 5, 6 }, cooldown = { 9, 8.5, 8, 7.5, 7 },
|
||||
mastery = { text = "Each strike also jumps to a second whelp nearby.", jump = 5 },
|
||||
},
|
||||
-- Polymorph (118): the toughest enemies in range become Polymorphed Sheep
|
||||
-- (creature 16372, display 856) that wander, harmless, until it wears off.
|
||||
polymorph = {
|
||||
spell = 118, display = 856, blurb = "Turns the toughest enemies in range into harmless sheep.",
|
||||
color = { 0.85, 0.55, 1 }, kind = "polymorph", pick = "strongest", range = 20,
|
||||
count = { 1, 1, 2, 2, 3 }, duration = { 4, 5, 5, 6, 6 }, cooldown = { 10, 9.5, 9, 8.5, 8 },
|
||||
mastery = { text = "Every whelp near each target becomes a sheep too.", radius = 3 },
|
||||
},
|
||||
-- Arcane Missiles (5143): a channel (kit 729) that fires the real Arcane
|
||||
-- Missile (7268) at its target each second, for 3 s (duration 27).
|
||||
arcanemissiles = {
|
||||
spell = 5143, missile = 7268, blurb = "Channels a missile into the toughest enemy in range every second.",
|
||||
color = { 0.85, 0.55, 1 }, kind = "channel", pick = "strongest", range = 22,
|
||||
damage = { 14, 18, 23, 29, 36 }, duration = { 3, 3, 4, 4, 5 }, cooldown = { 6, 5.5, 5, 4.5, 4 },
|
||||
mastery = { text = "Each missile splits into three, at the enemies nearest its target.", split = 3 },
|
||||
},
|
||||
-- Hellfire (1949): a channel on yourself (kit 4809). Each second its
|
||||
-- trigger, Hellfire Effect (5857), bursts around you (cast kit 844) and
|
||||
-- burns the brood in reach, each hit lit by its impact kit (845); and it
|
||||
-- burns you, as the spell's own damage aura (3) does.
|
||||
hellfire = {
|
||||
spell = 1949, tick = 5857, impactKit = 845,
|
||||
blurb = "Channels fire around you that burns the brood close by, and you with it.",
|
||||
color = { 1, 0.45, 0.15 }, kind = "channel", self = true,
|
||||
damage = { 12, 15, 19, 24, 30 }, radius = { 6, 6, 7, 7, 8 }, selfBurn = { 3, 3, 3, 3, 3 },
|
||||
duration = { 4, 4, 5, 5, 6 }, cooldown = { 9, 8.5, 8, 7.5, 7 },
|
||||
mastery = { text = "The fire heals you instead of burning you.", heal = 3 },
|
||||
},
|
||||
-- Evolutions (EVOLUTIONS): a spell at rank V and the passive it needs
|
||||
-- become one of these, in the spell's place. Each has one rank and is
|
||||
-- never offered on its own.
|
||||
-- Frostbolt Volley (8398): the real volley's frostbolts, one at each
|
||||
-- target at once; each chills.
|
||||
frostvolley = {
|
||||
spell = 8398, evolved = true, blurb = "Frostbolts fly at many enemies at once, chilling each one.",
|
||||
color = { 0.55, 0.8, 1 }, kind = "missile", volley = true, pick = "nearest", range = 24,
|
||||
-- Four bolts a volley: each missile starts its own flight loop at the
|
||||
-- hand (PlaySoundLoop3D, as in the world), and eight at once roared.
|
||||
damage = { 32 }, count = { 4 }, slow = { 0.5 }, cooldown = { 0.9 },
|
||||
},
|
||||
-- Shadow Bolt Volley (9081): heavy bolts at the toughest enemies at once.
|
||||
shadowvolley = {
|
||||
spell = 9081, evolved = true, blurb = "Heavy shadow bolts fly at the toughest enemies at once.",
|
||||
color = { 0.8, 0.45, 1 }, kind = "missile", volley = true, pick = "strongest", range = 26,
|
||||
damage = { 110 }, count = { 5 }, cooldown = { 2.2 },
|
||||
},
|
||||
-- Siphon Life (18265): Corruption's rot that heals you as it ticks.
|
||||
siphonlife = {
|
||||
spell = 18265, evolved = true, blurb = "Rots enemies every second and heals you as it does. It spreads when they die.",
|
||||
color = { 0.55, 0.85, 0.3 }, kind = "dot", pick = "nearest", range = 22, hop = 6,
|
||||
damage = { 18 }, heal = { 0.3 }, count = { 6 }, spread = { 3 }, duration = { 8 }, cooldown = { 2.2 },
|
||||
},
|
||||
-- Divine Shield (642): nothing hurts you while its bubble (state kit 252)
|
||||
-- lasts.
|
||||
divineshield = {
|
||||
spell = 642, evolved = true, blurb = "For a few seconds nothing can hurt you.",
|
||||
color = { 1, 0.9, 0.5 }, kind = "shield", immune = true, duration = { 5 }, cooldown = { 18 },
|
||||
},
|
||||
-- War Stomp (45): stuns everything around you (its state kit 349 pins
|
||||
-- Stun), and its cast kit (356) shakes the camera.
|
||||
warstomp = {
|
||||
spell = 45, evolved = true, blurb = "Stomps the ground, hurting and stunning everything around you.",
|
||||
color = { 1, 0.6, 0.2 }, kind = "nova", damage = { 45 }, radius = { 8 }, root = { 1.5 }, cooldown = { 3 },
|
||||
},
|
||||
-- Flamestrike (2120): a large burning ground area on the thickest pack.
|
||||
flamestrike = {
|
||||
spell = 2120, evolved = true, blurb = "A pillar of fire scorches the thickest pack and leaves the ground burning.",
|
||||
color = { 1, 0.45, 0.15 }, kind = "area", place = "pack", range = 22,
|
||||
damage = { 40 }, radius = { 8 }, duration = { 5 }, cooldown = { 5 },
|
||||
},
|
||||
-- Death Coil (6789): a missile that harms its target, heals you, and
|
||||
-- fears the target for 3 s (duration 27).
|
||||
deathcoil = {
|
||||
spell = 6789, blurb = "A coil of shadow harms the nearest enemy, heals you and sends it fleeing.",
|
||||
color = { 0.55, 0.85, 0.3 }, kind = "missile", pick = "nearest", range = 22,
|
||||
damage = { 25, 32, 40, 50, 62 }, heal = { 4, 5, 6, 7, 8 }, fear = { 3, 3, 3, 3, 3 },
|
||||
cooldown = { 7, 6.5, 6, 5.5, 5 },
|
||||
mastery = { text = "The coil fears every enemy around its target.", radius = 5 },
|
||||
},
|
||||
-- Cones (Kinds.lua): everything within `length` yards in front of you,
|
||||
-- inside CONE_ARC of your facing. Cone of Cold's cast kit (1027) holds
|
||||
-- its cone at the hand.
|
||||
conecold = {
|
||||
spell = 120, blurb = "A cone of frost in front of you hurts and chills the brood.",
|
||||
color = { 0.6, 0.85, 1 }, kind = "cone",
|
||||
damage = { 20, 26, 32, 40, 50 }, length = { 10, 10, 11, 11, 12 }, slow = { 0.6, 0.55, 0.5, 0.45, 0.4 },
|
||||
cooldown = { 4, 3.7, 3.4, 3.1, 2.8 },
|
||||
mastery = { text = "The cone freezes what it hits for 1 second.", freeze = 1 },
|
||||
},
|
||||
-- Turrets (Kinds.lua): a totem that shoots its `bolt` at the nearest
|
||||
-- enemy in range every `every` seconds. Searing Totem is the creature
|
||||
-- 2523 (display 4589); its bolt is Searing Bolt (3606).
|
||||
searing = {
|
||||
spell = 3599, bolt = 3606, display = 4589, blurb = "Plants a totem that shoots fire at the nearest enemy.",
|
||||
color = { 1, 0.5, 0.15 }, kind = "turret", range = 20, every = 1,
|
||||
damage = { 10, 13, 16, 20, 25 }, duration = { 12, 14, 16, 18, 20 }, cooldown = { 12, 11.5, 11, 10.5, 10 },
|
||||
mastery = { text = "The totem shoots two bolts at once.", bolts = 2 },
|
||||
},
|
||||
-- Zones (Kinds.lua): a totem whose aura slows every enemy near it, showing
|
||||
-- the aura's visual on each. Earthbind Totem is the creature 2630
|
||||
-- (display 4588); its aura is Earthbind (3600).
|
||||
earthbind = {
|
||||
spell = 2484, aura = 3600, display = 4588, blurb = "Plants a totem that slows every enemy near it.",
|
||||
color = { 0.8, 0.65, 0.4 }, kind = "zone",
|
||||
radius = { 8, 8.5, 9, 9.5, 10 }, slow = { 0.5, 0.45, 0.4, 0.35, 0.3 }, duration = { 10, 11, 12, 13, 14 },
|
||||
cooldown = { 14, 13, 12, 11, 10 },
|
||||
mastery = { text = "Every 3 seconds the totem roots everything near it for 1 second.", root = 1, every = 3 },
|
||||
},
|
||||
-- Traps (Kinds.lua): a hunter's trap (its gameobject's model) at your
|
||||
-- feet. Freezing Trap's effect is Freezing Trap Effect (3355: its state
|
||||
-- kit 3709 is the ice block); Explosive Trap's is Explosive Trap Effect
|
||||
-- (13812), whose ground area burns where it went off.
|
||||
freezetrap = {
|
||||
spell = 1499, effect = 3355, path = "World\\Goober\\G_FreezingTrap.m2", sound = 6522,
|
||||
blurb = "Drops a trap at your feet that freezes the first whelp to step on it, and those beside it.",
|
||||
color = { 0.6, 0.85, 1 }, kind = "trap",
|
||||
radius = { 2, 2.5, 3, 3.5, 4 }, freeze = { 3, 3.5, 4, 4.5, 5 }, traps = { 1, 1, 2, 2, 3 },
|
||||
cooldown = { 8, 7.5, 7, 6.5, 6 },
|
||||
mastery = { text = "Each trap springs twice.", springs = 2 },
|
||||
},
|
||||
explosivetrap = {
|
||||
spell = 13813, effect = 13812, path = "World\\Goober\\G_ExplosiveTrap.m2", sound = 6523,
|
||||
blurb = "Drops a trap at your feet that blows up under the first whelp to step on it and sets the brood alight.",
|
||||
color = { 1, 0.5, 0.15 }, kind = "trap", knock = { 6, 8 },
|
||||
damage = { 30, 40, 50, 62, 75 }, radius = { 5, 5.5, 6, 6.5, 7 }, ignite = { 5, 6, 8, 10, 12 }, igniteSeconds = 4,
|
||||
traps = { 1, 1, 2, 2, 3 }, cooldown = { 8, 7.5, 7, 6.5, 6 },
|
||||
mastery = { text = "The ground it blasts burns for 4 seconds.", area = 4 },
|
||||
},
|
||||
-- Pyroblast (11366): a slow, heavy fireball at the toughest enemy, which
|
||||
-- leaves it burning.
|
||||
pyroblast = {
|
||||
spell = 11366, blurb = "A great fireball at the toughest enemy in range, which leaves it burning.",
|
||||
color = { 1, 0.45, 0.15 }, kind = "missile", pick = "strongest", range = 26,
|
||||
damage = { 80, 105, 130, 160, 200 }, ignite = { 6, 8, 10, 12, 15 }, igniteSeconds = 4,
|
||||
cooldown = { 5, 4.7, 4.4, 4.1, 3.8 },
|
||||
mastery = { text = "The blast sets every enemy within 4 yards of its target alight.", radius = 4 },
|
||||
},
|
||||
-- Charge (100): you rush into the thickest pack in range (its cast kit
|
||||
-- 44: the trail at the chest, the dust at the base), stunning what is
|
||||
-- around you where you land (Charge Stun 7922: state kit 349).
|
||||
charge = {
|
||||
spell = 100, stunAura = 7922, blurb = "You rush into the thickest pack in range and stun the brood where you land.",
|
||||
color = { 1, 0.6, 0.2 }, kind = "charge",
|
||||
range = { 12, 13, 14, 15, 16 }, damage = { 20, 26, 32, 40, 50 }, radius = { 4, 4, 4.5, 4.5, 5 },
|
||||
stun = { 1, 1.2, 1.4, 1.6, 2 }, cooldown = { 8, 7.5, 7, 6.5, 6 },
|
||||
mastery = { text = "Where you land, the brood is thrown back.", knock = { 10, 6 } },
|
||||
},
|
||||
-- Whirlwind (1680): a nova; its cast kit (369) spins you (126) with the
|
||||
-- whirlwind at your feet.
|
||||
whirlwind = {
|
||||
spell = 1680, blurb = "You spin with your weapon, cutting everything close around you.",
|
||||
color = { 1, 0.75, 0.4 }, kind = "nova",
|
||||
damage = { 18, 24, 30, 38, 48 }, radius = { 5, 5.2, 5.4, 5.7, 6 }, cooldown = { 3, 2.8, 2.6, 2.4, 2.2 },
|
||||
mastery = { text = "You spin twice.", echo = true },
|
||||
},
|
||||
-- Mind Blast (8092) and Moonfire (8921) are instant: their impacts land
|
||||
-- at once. Moonfire leaves an arcane burn.
|
||||
mindblast = {
|
||||
spell = 8092, blurb = "Blasts the mind of the toughest enemy in range.",
|
||||
color = { 0.75, 0.5, 1 }, kind = "missile", pick = "strongest", range = 24,
|
||||
damage = { 40, 52, 65, 80, 100 }, count = { 1, 1, 1, 1, 2 }, cooldown = { 4, 3.7, 3.4, 3.1, 2.8 },
|
||||
mastery = { text = "It blasts two enemies." },
|
||||
},
|
||||
moonfire = {
|
||||
spell = 8921, blurb = "Moonlight strikes enemies at random and leaves them burning.",
|
||||
color = { 0.85, 0.6, 1 }, kind = "missile", pick = "random", range = 24,
|
||||
damage = { 12, 15, 18, 22, 27 }, count = { 1, 2, 2, 3, 3 }, ignite = { 4, 5, 6, 7, 8 }, igniteSeconds = 4,
|
||||
cooldown = { 3, 2.8, 2.6, 2.4, 2.2 },
|
||||
mastery = { text = "Its burn lasts twice as long.", longer = 2 },
|
||||
},
|
||||
-- Fusions (FUSIONS): two spells at rank V become one of these. Like an
|
||||
-- evolution, each has one rank and is never offered on its own.
|
||||
-- Fire Nova Totem (1535, creature 5879: display 4683): a turret that also
|
||||
-- burns everything around it, as Magma Totem's pulse does.
|
||||
firenova = {
|
||||
spell = 1535, bolt = 3606, pulse = 8187, display = 4683, evolved = true, fused = true,
|
||||
blurb = "Plants a totem that shoots fire at the brood and burns everything around it.",
|
||||
color = { 1, 0.5, 0.15 }, kind = "turret", range = 22, every = 0.6, pulseEvery = 2, bolts = 2,
|
||||
damage = { 30 }, pulseDamage = { 30 }, radius = { 8 }, duration = { 20 }, cooldown = { 12 },
|
||||
},
|
||||
-- Frost Trap (13809): a trap that freezes everything around it and leaves
|
||||
-- its icy ground (Frost Trap Aura 13810: frosttrap_aura.mdx) slowing the
|
||||
-- brood.
|
||||
frosttrap = {
|
||||
spell = 13809, effect = 3355, area = 13810, path = "World\\Goober\\G_FrostTrap.m2", sound = 6524,
|
||||
evolved = true, fused = true,
|
||||
blurb = "Drops traps that blow up, freeze everything around them, and leave the ground icy.",
|
||||
color = { 0.6, 0.85, 1 }, kind = "trap", knock = { 4, 6 },
|
||||
damage = { 60 }, radius = { 7 }, freeze = { 3 }, slow = { 0.4 }, duration = { 6 }, traps = { 3 }, cooldown = { 5 },
|
||||
},
|
||||
-- Intercept (20252): Charge's rush (cast kit 44) that lands in a
|
||||
-- Whirlwind.
|
||||
intercept = {
|
||||
spell = 20252, stunAura = 7922, whirl = 1680, evolved = true, fused = true,
|
||||
blurb = "You rush into the thickest pack, spin through it and stun it.",
|
||||
color = { 1, 0.6, 0.2 }, kind = "charge",
|
||||
range = { 18 }, damage = { 60 }, radius = { 6 }, stun = { 2 }, cooldown = { 4 },
|
||||
},
|
||||
-- Starfire (2912): heavy arcane strikes at random that leave a burn.
|
||||
starfire = {
|
||||
spell = 2912, evolved = true, fused = true,
|
||||
blurb = "Starlight crashes down on enemies at random and leaves them burning.",
|
||||
color = { 0.85, 0.6, 1 }, kind = "missile", pick = "random", range = 26,
|
||||
damage = { 55 }, count = { 4 }, ignite = { 10 }, igniteSeconds = 6, cooldown = { 2 },
|
||||
},
|
||||
-- Frost Breath (3129): a wide cone from your breath (its cast kit 2610:
|
||||
-- FrostBreath.mdx at the breath) that freezes.
|
||||
frostbreath = {
|
||||
spell = 3129, evolved = true, fused = true,
|
||||
blurb = "You breathe frost over everything in front of you, freezing it.",
|
||||
color = { 0.6, 0.85, 1 }, kind = "cone", wide = 1.5,
|
||||
damage = { 60 }, length = { 14 }, freeze = { 2 }, cooldown = { 3 },
|
||||
},
|
||||
-- Shadow Flame (22539): Nefarian's breath, a cone of shadowfire that leaves
|
||||
-- the brood burning (its impact kit 5870 on each). Its flame hangs at an
|
||||
-- attachment a character has none of, so the impacts show the cone.
|
||||
shadowflame = {
|
||||
spell = 22539, evolved = true, fused = true,
|
||||
blurb = "A cone of shadowflame in front of you that leaves the brood burning.",
|
||||
color = { 0.75, 0.45, 1 }, kind = "cone", wide = 1.3, burnSchool = "Fire", -- its burn is flame
|
||||
damage = { 70 }, length = { 13 }, ignite = { 15 }, igniteSeconds = 4, cooldown = { 3 },
|
||||
},
|
||||
}
|
||||
local SPELL_ORDER = {
|
||||
"frostbolt", "shadowbolt", "lightning", "chain", "corruption", "nova", "holynova", "thunder", "totem", "blizzard",
|
||||
"rainoffire", "consecration", "shield", "thorns", "frostnova", "scream", "demoshout", "stoneclaw", "blastwave", "imp",
|
||||
"drainlife", "mindflay", "inferno", "lshield", "polymorph", "arcanemissiles", "deathcoil", "hellfire",
|
||||
"conecold", "searing", "earthbind", "freezetrap", "explosivetrap", "pyroblast", "charge", "whirlwind",
|
||||
"mindblast", "moonfire",
|
||||
-- The evolutions and fusions, on the bar after the others.
|
||||
"frostvolley", "shadowvolley", "siphonlife", "divineshield", "warstomp", "flamestrike",
|
||||
"firenova", "frosttrap", "intercept", "starfire", "frostbreath", "shadowflame",
|
||||
}
|
||||
-- A spell at rank V with the passive it `needs` (any rank) evolves `into` a
|
||||
-- stronger spell, which takes its place.
|
||||
local EVOLUTIONS = {
|
||||
frostbolt = { needs = "multishot", into = "frostvolley" },
|
||||
shadowbolt = { needs = "might", into = "shadowvolley" },
|
||||
corruption = { needs = "vampiric", into = "siphonlife" },
|
||||
shield = { needs = "vigor", into = "divineshield" },
|
||||
thunder = { needs = "devotion", into = "warstomp" },
|
||||
rainoffire = { needs = "amplify", into = "flamestrike" },
|
||||
}
|
||||
-- Two spells both at rank V fuse `into` one stronger spell, which takes the
|
||||
-- first one's place and frees the second one's slot.
|
||||
local FUSIONS = {
|
||||
{ a = "searing", b = "totem", into = "firenova" },
|
||||
{ a = "freezetrap", b = "explosivetrap", into = "frosttrap" },
|
||||
{ a = "charge", b = "whirlwind", into = "intercept" },
|
||||
{ a = "moonfire", b = "lightning", into = "starfire" },
|
||||
{ a = "conecold", b = "frostnova", into = "frostbreath" },
|
||||
{ a = "mindblast", b = "pyroblast", into = "shadowflame" },
|
||||
}
|
||||
local MAX_SPELLS = 6
|
||||
local EARLY_SPELLS = 3 -- until you have this many spells, each level offers a new one
|
||||
-- The first spell of a run, by class (UnitClass's English name). The start
|
||||
-- offers your class's and two others' (Hud.lua's BuildOffers); a class with
|
||||
-- none here (a rogue) gets three others.
|
||||
local STARTERS = {
|
||||
MAGE = "frostbolt", WARLOCK = "shadowbolt", SHAMAN = "lightning", PRIEST = "holynova", DRUID = "moonfire",
|
||||
PALADIN = "consecration", WARRIOR = "thunder", HUNTER = "explosivetrap",
|
||||
}
|
||||
|
||||
-- `icon` is the spell whose icon the passive shows; `total` is what all its
|
||||
-- ranks give together.
|
||||
local PASSIVES = {
|
||||
might = { name = "Might", icon = 19740, text = "+15% damage", total = "+%d%% damage", per = 15 },
|
||||
haste = { name = "Haste", icon = 12043, text = "-8% spell cooldowns", total = "-%d%% cooldowns", per = 8 },
|
||||
reach = { name = "Reach", icon = 2481, text = "+40% gem pickup radius", total = "+%d%% pickup radius", per = 40 },
|
||||
vigor = { name = "Vigor", icon = 1243, text = "+25 maximum health, and heal 25", total = "+%d maximum health", per = 25 },
|
||||
renewal = { name = "Renewal", icon = 139, text = "+1.2 health per second", total = "%.1f health per second", per = 1.2 },
|
||||
sprint = { icon = 2983, text = "+8% run speed", total = "+%d%% run speed", per = 8 },
|
||||
devotion = { icon = 465, text = "-5% damage taken", total = "-%d%% damage taken", per = 5 },
|
||||
amplify = { icon = 1008, text = "+10% spell radius", total = "+%d%% spell radius", per = 10 },
|
||||
multishot = {
|
||||
icon = 2643, text = "+1 target for bolts and Corruption", total = "+%d targets", per = 1,
|
||||
max = 3,
|
||||
},
|
||||
wisdom = { icon = 19742, text = "+15% experience", total = "+%d%% experience", per = 15 },
|
||||
soulstone = {
|
||||
name = "Soulstone", icon = 20707, text = "Once per run, rise again with half your health",
|
||||
total = "%d revive", per = 1, max = 1,
|
||||
},
|
||||
vampiric = {
|
||||
icon = 15286,
|
||||
text = "Heals you for 0.2% of the damage you deal, at most 2 health a second",
|
||||
total = "up to %d health a second", per = 2,
|
||||
},
|
||||
}
|
||||
-- A school's colour for its damage numbers, as the combat log colours it
|
||||
-- (COMBATLOG_DEFAULT_COLORS). `school`: GetSpellSchool's name of it.
|
||||
local SCHOOL_MASKS = {
|
||||
Physical = SCHOOL_MASK_PHYSICAL, Holy = SCHOOL_MASK_HOLY, Fire = SCHOOL_MASK_FIRE, Nature = SCHOOL_MASK_NATURE,
|
||||
Frost = SCHOOL_MASK_FROST, Shadow = SCHOOL_MASK_SHADOW, Arcane = SCHOOL_MASK_ARCANE,
|
||||
}
|
||||
local COMBAT_LOG_SETTINGS = { colors = COMBATLOG_DEFAULT_COLORS }
|
||||
local schoolColors = {}
|
||||
|
||||
local function SchoolColor(school)
|
||||
local color = schoolColors[school];
|
||||
if not color then
|
||||
local c = CombatLogUtil.GetColorBySchool(SCHOOL_MASKS[school], COMBAT_LOG_SETTINGS);
|
||||
color = { c.r, c.g, c.b };
|
||||
schoolColors[school] = color;
|
||||
end
|
||||
return color;
|
||||
end
|
||||
|
||||
-- The names are the client's own, in its language: each spell's from its
|
||||
-- spell, and each passive without a name of its own from its icon's spell.
|
||||
-- A spell's `color` marks it in the HUD; its hits show in its school's
|
||||
-- colour (`hitColor`).
|
||||
for _, def in pairs(SPELLS) do
|
||||
def.name = C_Spell.GetSpellName(def.spell)
|
||||
-- The spell's school, which the combos read: "Fire", "Frost", ...
|
||||
local _, school = GetSpellSchool(def.spell)
|
||||
def.school = school
|
||||
def.hitColor = SchoolColor(school)
|
||||
end
|
||||
for _, p in pairs(PASSIVES) do
|
||||
p.name = p.name or C_Spell.GetSpellName(p.icon)
|
||||
end
|
||||
|
||||
local HEAL_ICON = 5185 -- Healing Touch
|
||||
local UNKNOWN_ICON = "Interface\\Icons\\INV_Misc_QuestionMark"
|
||||
local PASSIVE_ORDER = {
|
||||
"might", "haste", "reach", "vigor", "renewal", "sprint", "vampiric", "devotion", "amplify", "multishot",
|
||||
"wisdom", "soulstone",
|
||||
}
|
||||
local VAMPIRIC_SHARE = 0.002 -- of the damage dealt, per rank
|
||||
local HOLY_NOVA_HEAL_TARGETS = 8 -- Holy Nova heals for at most this many enemies
|
||||
local SOULSTONE_SPELL = 20707 -- Soulstone Resurrection, the revive's visual
|
||||
local SOULSTONE_BLAST = 10 -- yards: rising again throws back the enemies this close
|
||||
local MAX_LEVEL = 5
|
||||
|
||||
local ROMAN = { "I", "II", "III", "IV", "V" }
|
||||
|
||||
-- The bars ease to a new value in the engine (StatusBar interpolation); a
|
||||
-- reset (a new run, a level's wrap) shows at once.
|
||||
local EASE = Enum.StatusBarInterpolation.ExponentialEaseOut
|
||||
local AT_ONCE = Enum.StatusBarInterpolation.Immediate
|
||||
|
||||
-- The items' tuning.
|
||||
local TUNING = {
|
||||
SWIFTNESS_FACTOR = 1.5,
|
||||
SWIFTNESS_SECONDS = 10,
|
||||
ITEM_CHANCE = 1 / 150, -- a whelp's chance to drop an item
|
||||
ITEM_REACH = 1.5, -- yards: walking this close uses a drop
|
||||
ITEM_SECONDS = 30, -- a drop lies this long
|
||||
ITEM_ICON = 28, -- pixels
|
||||
ITEM_COLOR = { 1, 0.82, 0 },
|
||||
CHEST_REACH = 2, -- yards: walking this close opens a chest
|
||||
CHEST_AWAY = 2.5, -- yards from the elite's corpse that its chest lands
|
||||
CHEST_LINGER = 4, -- seconds an open chest stays before it fades
|
||||
EGG_FIRST = 60, -- the second of the run the first clutch is laid
|
||||
EGG_EVERY = 75, -- seconds between clutches
|
||||
EGG_CLUTCH = 4, -- eggs in a clutch
|
||||
EGG_SPREAD = 2.5, -- yards from the clutch's middle to each egg
|
||||
EGG_HATCH = 20, -- seconds an egg takes to hatch
|
||||
EGG_WARN = 5, -- seconds before it hatches that a red ring marks it
|
||||
EGG_WHELPS = 4, -- whelps that hatch from each egg
|
||||
-- Goblin Land Mines (the creature 7527's display 6271: Stand 0, Despawn
|
||||
-- 157 of 1.17 s). The hero stepping within MINE_TRIGGER arms one; then a
|
||||
-- whelp stepping within it sets it off: it casts the Goblin Sapper
|
||||
-- Charge's visual (13241) at its own point, which plants Bomb_ExplosionA
|
||||
-- there and plays the FireNova cast kit.
|
||||
MINE_DISPLAY = 6271,
|
||||
MINE_SPELL = 13241,
|
||||
MINE_ARM_SOUND = 3216, -- Land Mine Arming's (11816) cast kit 259 sound
|
||||
MINE_COUNT = 4, -- mines lying in the lair
|
||||
MINE_RESPAWN = 15, -- seconds before a spent mine is laid again
|
||||
MINE_TRIGGER = 1.5, -- yards: the hero this close arms it, then a whelp sets it off
|
||||
MINE_RADIUS = 6, -- yards the blast reaches
|
||||
MINE_DAMAGE = 60, -- grows with the run as the brood's health does
|
||||
MINE_KNOCK = { 12, 8 },
|
||||
MINE_DESPAWN = 1.17, -- seconds of Despawn, then the spent mine hides
|
||||
MINE_COLOR = { 1, 0.5, 0.1 },
|
||||
-- The lair's lava cracks (VENTS). From VENT_FIRST, every VENT_EVERY
|
||||
-- seconds a wave of cracks is marked by a red ring for VENT_WARN
|
||||
-- seconds, then erupts: its Custom0 (153, 1.57 s) and Onyxia's Eruption
|
||||
-- sound (spell 17731's cast kit 4409: SoundEntries 7714). The eruption
|
||||
-- burns everything within VENT_RADIUS (times the crack's scale), you too.
|
||||
VENT_FIRST = 120,
|
||||
VENT_EVERY = 8,
|
||||
VENT_WARN = 2,
|
||||
VENT_ERUPT = 1.57,
|
||||
VENT_SOUND = 7714,
|
||||
VENT_RADIUS = 4.5,
|
||||
VENT_DAMAGE = 50, -- to the brood, grown with the run as its health is
|
||||
VENT_HERO = 0.15, -- of your maximum health
|
||||
VENT_KNOCK = { 3, 12 }, -- mostly up
|
||||
VENT_WAVE_GROWTH = 150, -- seconds after VENT_FIRST for each crack more in a wave
|
||||
VENT_WAVE_MAX = 4,
|
||||
VENT_COLOR = { 1, 0.35, 0 },
|
||||
-- Combos (Brood.lua's SpellDamage, by the spells' schools).
|
||||
SHATTER = 3, -- a hit but frost on a frozen enemy, times
|
||||
DREAD = 1.5, -- shadow on a fleeing enemy, times
|
||||
DETONATE_RADIUS = 4, -- yards a detonated burn splashes, at half
|
||||
STEAM_STUN = 1.5, -- seconds frost on a fire burn stuns
|
||||
WILDFIRE_RADIUS = 4, -- yards a burning enemy's death sets alight
|
||||
COMBO_COLOR = { 1, 0.9, 0.3 },
|
||||
BURN_AURA = 348, -- Immolate: its state kit 235 burns at the base
|
||||
STUN_AURA = 20549, -- War Stomp's StunSwirl (no Gouge roar)
|
||||
-- Traps (Kinds.lua): a trap lands at your feet, arms after TRAP_ARM, and
|
||||
-- springs on the first whelp within TRAP_TRIGGER: its gameobject's
|
||||
-- Custom0 (153) with that slot's sound, then it hides.
|
||||
TRAP_ARM = 1,
|
||||
TRAP_TRIGGER = 1.5,
|
||||
TRAP_LIFE = 30, -- seconds an unsprung trap lies
|
||||
TRAP_MAX = 6, -- traps lying at once; the oldest goes first
|
||||
TRAP_SPRUNG = 1.2, -- seconds the sprung trap shows before it hides
|
||||
TRAP_SCALE = 2, -- the trap models are a yard across; this shows them from the camera
|
||||
-- Charge (Kinds.lua): you rush at this many yards a second; the dash ends
|
||||
-- at the target or after CHARGE_MAX seconds.
|
||||
CHARGE_SPEED = 28,
|
||||
CHARGE_MAX = 1,
|
||||
CONE_ARC = math.pi / 2, -- radians a cone spans, centred on its aim
|
||||
CAST_POSE = 0.4, -- seconds a cone's cast holds you facing it
|
||||
-- The grace when the last level-up or chest card closes (Hud.lua).
|
||||
GRACE_SECONDS = 1.5, -- nothing hurts you this long
|
||||
GRACE_RADIUS = 6, -- yards: the brood this close is thrown back
|
||||
GRACE_KNOCK = { 12, 6 },
|
||||
GRACE_ALPHA = 0.5, -- how you show while it lasts
|
||||
-- Off-screen markers (Markers.lua): a closed chest or a clutch past the
|
||||
-- scene's edge shows an arrow at the edge, pointing at it, and its icon.
|
||||
MARKER_ARROW = "Interface\\ChatFrame\\ChatFrameExpandArrow", -- points right
|
||||
MARKER_ARROW_SIZE = 24, -- pixels
|
||||
MARKER_ICON_SIZE = 26, -- pixels
|
||||
MARKER_MARGIN = 36, -- pixels from the scene's edge to the arrow's centre
|
||||
POTION_HEAL = 0.3, -- of the maximum health
|
||||
FIREWARD_SHARE = 0.6, -- of the maximum health: the fire a Fire Protection Potion soaks
|
||||
FIREWARD_SECONDS = 60,
|
||||
INVULNERABLE_SECONDS = 6, -- a Limited Invulnerability Potion: no bites, no charges
|
||||
BOMB_COUNT = 3, -- Goblin Bomb Dispenser: walking bombs (Pet Bomb's display)
|
||||
BOMB_DISPLAY = 8189,
|
||||
BOMB_SPEED = 8,
|
||||
BOMB_LIFE = 12, -- seconds; a bomb with no prey goes off where it stands
|
||||
BOMB_TRIGGER = 1.5, -- yards from its prey
|
||||
BOMB_RADIUS = 4,
|
||||
BOMB_DAMAGE = 50, -- grows with the run
|
||||
BOMB_SPELL = 13241, -- the land mine's blast
|
||||
BOMB_KNOCK = { 10, 7 },
|
||||
SAPPER_RADIUS = 10,
|
||||
SAPPER_SELF = 0.15, -- of the maximum health; it never kills you
|
||||
SAPPER_ELITE = 0.25, -- of an elite's maximum health
|
||||
CHICKEN_DISPLAY = 6909, -- the robot chicken (ChickenSkinRobot)
|
||||
CHICKEN_SECONDS = 20,
|
||||
CHICKEN_SPEED = 9,
|
||||
CHICKEN_DAMAGE = 12, -- a peck, grows with the run
|
||||
CHICKEN_PECK = 0.8, -- seconds between pecks
|
||||
CHICKEN_REACH = 1.8,
|
||||
CHICKEN_LEASH = 20, -- yards from the hero: it hunts no farther
|
||||
CHICKEN_HUNT_MIN = 6, -- yards from the hero: it prefers prey past this
|
||||
CHICKEN_TAUNT = 3, -- seconds a peck holds the whelps around it
|
||||
CHICKEN_TAUNT_RADIUS = 3, -- yards around the chicken that a peck taunts
|
||||
CHICKEN_ROAM = 6, -- yards around the hero that it roams with no prey
|
||||
-- Knockbacks (actor:KnockBack): yards a second out and up.
|
||||
SOULSTONE_KNOCK = { 16, 10 },
|
||||
FEAR_STEP = 6, -- yards a fleeing enemy runs before its next MoveTo
|
||||
IMP_SPEED = 8.5,
|
||||
IMP_FOLLOW = 3, -- yards: farther from you than this, the imp follows
|
||||
INFERNO_FALL = 1.0, -- seconds from the cast until the meteor strikes the ground
|
||||
INFERNO_STUN = 2, -- Inferno Effect's stun (22703, duration 39)
|
||||
INFERNAL_ROOT = 1.7, -- the Infernal's landing (22707, duration 487)
|
||||
INFERNAL_SPEED = 7,
|
||||
INFERNAL_REACH = 2.5,
|
||||
INFERNAL_SWING = 2, -- seconds between two blows
|
||||
INFERNAL_BURN_RADIUS = 4, -- yards its Immolation burns, once a second
|
||||
INFERNAL_TAUNT = 3, -- seconds a burn holds the whelps on it
|
||||
ZAP_GAP = 0.5, -- seconds between two of Lightning Shield's strikes
|
||||
SHEEP_SPEED = 1.5,
|
||||
SHEEP_WANDER = 2, -- yards a sheep ambles before its next MoveTo
|
||||
HELLFIRE_IMPACTS = 8, -- hit whelps a Hellfire tick lights with its impact kit
|
||||
}
|
||||
-- `itemID` is the real item (its name and icon), `spell` the visual its use plays.
|
||||
local ITEMS = {
|
||||
{ key = "potion", itemID = 929, spell = 17534, weight = 30 },
|
||||
-- The grenade stuns with War Stomp's silent StunSwirl (20549): its own
|
||||
-- stun kit (19769's 780) plays Gouge on every whelp it holds, with no
|
||||
-- duplicate guard, a roar over a whole pack.
|
||||
-- A thrown item lands on the thickest pack: `radius`, `damage` (grown with
|
||||
-- the run as the brood's health is), a `stun` and a `knock`.
|
||||
{ key = "grenade", itemID = 15993, spell = 19769, stunAura = 20549, weight = 25,
|
||||
radius = 5, damage = 45, stun = 2, knock = { 10, 7 } },
|
||||
-- Dense Dynamite (its throw, 23063): wider and harder, with no stun.
|
||||
{ key = "dynamite", itemID = 18641, spell = 23063, weight = 12, radius = 7, damage = 75, knock = { 14, 9 } },
|
||||
-- The potions' drink (their spells' cast kit 704, a vial in hand).
|
||||
{ key = "fireward", itemID = 13457, spell = 17543, weight = 8 },
|
||||
{ key = "invulnerable", itemID = 3387, spell = 3169, weight = 6 },
|
||||
{ key = "bombs", itemID = 10587, weight = 8 },
|
||||
{ key = "swift", itemID = 2459, spell = 2379, weight = 15 },
|
||||
{ key = "hearth", itemID = 6948, spell = 8690, weight = 10 },
|
||||
{ key = "sapper", itemID = 10646, spell = 13241, weight = 10 },
|
||||
{ key = "chicken", itemID = 10725, spell = 23133, weight = 10 },
|
||||
}
|
||||
-- Chests: an elite drops one, and every GOLDEN_EVERY-th is golden. Walking up
|
||||
-- opens it (Open 148, then Opened 149 held, as the client's chests do), and
|
||||
-- the chest gives `picks` upgrade cards. `open` is the Open sequence's length
|
||||
-- in the model's file; `speed` plays it faster. The two models are about
|
||||
-- 1.3 and 3.1 yards across at scale 1.
|
||||
local CHEST_SOUND = 1277 -- ChestOpen (SoundEntries)
|
||||
local GOLDEN_EVERY = 5
|
||||
local CHESTS = {
|
||||
plain = { path = "World\\SkillActivated\\Containers\\TreasureChest01.m2", scale = 1.2, open = 0.666, speed = 1, picks = 1,
|
||||
title = "Treasure", icon = "Interface\\Icons\\INV_Box_01" },
|
||||
golden = { path = "World\\Goober\\G_ChestTitan.m2", scale = 0.6, open = 3.2, speed = 2, picks = 3,
|
||||
title = "Golden Treasure", icon = "Interface\\Icons\\INV_Box_02" },
|
||||
}
|
||||
-- Onyxia's Lair's lava cracks inside the arena: { x, y, z, facing, scale,
|
||||
-- mirrored }. They are the lava-trap gameobjects of the server's Onyxia
|
||||
-- encounter (177984-178020, Doodad_OnyziasLairLavaTrap* and *Mirror*, at
|
||||
-- AzerothCore's retail spawns on map 249), within 23 yards of CENTER. The
|
||||
-- crack's mesh lies under the floor: only its eruption shows.
|
||||
local VENT_PATH = "World\\Kalimdor\\OnyxiasLair\\LavaTraps\\OnyziasLairLavaTrap.m2"
|
||||
local VENT_MIRROR_PATH = "World\\Kalimdor\\OnyxiasLair\\LavaTraps\\OnyziasLairLavaTrapMirror.m2"
|
||||
local VENTS = {
|
||||
{ -1.67, -217.14, -85.67, 0.785, 1.00, false },
|
||||
{ -9.81, -212.03, -88.14, -0.262, 0.82, false },
|
||||
{ 13.19, -214.44, -86.48, 0.873, 0.82, false },
|
||||
{ 2.95, -198.74, -87.05, 1.484, 0.82, true },
|
||||
{ 17.00, -204.31, -86.22, -2.618, 1.00, true },
|
||||
{ 3.20, -230.38, -87.05, -1.484, 0.82, false },
|
||||
{ -19.70, -209.64, -88.55, -0.436, 1.00, false },
|
||||
{ 16.84, -225.11, -86.22, 2.618, 1.00, false },
|
||||
{ 14.30, -195.44, -86.03, 0.436, 0.85, true },
|
||||
{ -19.40, -219.85, -88.48, -2.793, 1.00, false },
|
||||
{ -15.67, -196.47, -89.10, 1.047, 1.00, true },
|
||||
{ -0.40, -189.26, -87.35, -1.396, 0.82, true },
|
||||
}
|
||||
-- Gear: kept for the run. Chests offer it (Hud.lua's BuildOffers): a golden
|
||||
-- chest's first pick, while you hold no weapon, is three of the weapons; any
|
||||
-- other chest pick may offer one piece of worn gear while you wear fewer
|
||||
-- than MAX_WORN. `slot`: "weapon" (one, in your hands: actor:TryOn) or
|
||||
-- "worn". `text`: what it does. The numbers of an effect that hurts grow
|
||||
-- with the run, as the brood's health does.
|
||||
local MAX_WORN = 3
|
||||
local GEAR = {
|
||||
-- Thunderfury's proc (21992, the item's chance-on-hit spell): its bolt
|
||||
-- leaps from enemy to enemy as Chain Lightning's does. `chance` a spell
|
||||
-- hit, at most once in `every` seconds.
|
||||
thunderfury = { itemID = 19019, slot = "weapon", spell = 21992, school = "Nature", chance = 0.2, every = 1.5,
|
||||
damage = 30, jumps = 4, hop = 8, slow = 0.6, slowSeconds = 3,
|
||||
text = "Your spell hits may loose a bolt of lightning that leaps to 4 more of the brood and slows them." },
|
||||
-- Sulfuras's proc (21162, a Fireball).
|
||||
sulfuras = { itemID = 17182, slot = "weapon", spell = 21162, school = "Fire", chance = 0.15, every = 2,
|
||||
damage = 40, radius = 5, burn = 8, burnSeconds = 4, knock = { 6, 5 },
|
||||
text = "Your spell hits may hurl a fireball that bursts and sets the brood around it burning." },
|
||||
-- Benediction and Anathema (18609) are one staff that turns, with their
|
||||
-- Call Anathema (23041) and Call Benediction (23042).
|
||||
benediction = { itemID = 18608, other = 18609, slot = "weapon", toAnathema = 23041, toBenediction = 23042,
|
||||
every = 20, bonus = 0.6, renew = 2,
|
||||
text = "Every 20 s the staff turns. Benediction: Holy spells hit 60% harder, and you heal 2 a second. "
|
||||
.. "Anathema: Shadow spells hit 60% harder." },
|
||||
-- Rhok'delar shoots Auto Shot (75); the bow shows in your hands as it does.
|
||||
rhokdelar = { itemID = 18713, slot = "weapon", spell = 75, school = "Physical", every = 0.8, range = 30, damage = 18,
|
||||
text = "Shoots the nearest of the brood within 30 yd every 0.8 s." },
|
||||
drakefire = { itemID = 16309, slot = "worn", share = 0.5,
|
||||
text = "Fire hurts you half as much: lava, fireballs, breath, your own Hellfire." },
|
||||
onyxiacloak = { itemID = 15138, slot = "worn", text = "A dragon's breath does you no harm." },
|
||||
hoj = { itemID = 11815, slot = "worn", chance = 0.1, text = "Each spell you cast has a 10% chance to recharge at once." },
|
||||
-- The on-use trinkets go off by themselves when the brood is within
|
||||
-- `near`, with their use spells' visuals.
|
||||
tep = { itemID = 18820, slot = "worn", spell = 23271, every = 90, seconds = 15, bonus = 0.4, near = 20,
|
||||
text = "Every 90 s, with the brood near, your spells hit 40% harder for 15 s." },
|
||||
zhc = { itemID = 19950, slot = "worn", spell = 24658, every = 120, seconds = 20, bonus = 0.8, fade = 0.05, near = 20,
|
||||
text = "Every 2 min, with the brood near, your spells hit 80% harder for 20 s, 5% less with each cast." },
|
||||
-- The talisman's dodge: the brood's blows (bites, charges) miss a share.
|
||||
drakefang = { itemID = 19406, slot = "worn", share = 0.1, text = "You dodge a tenth of the brood's blows: bites and charges hurt 10% less." },
|
||||
}
|
||||
local GEAR_ORDER = { "thunderfury", "sulfuras", "benediction", "rhokdelar", "drakefire", "onyxiacloak", "hoj", "tep", "zhc",
|
||||
"drakefang" }
|
||||
|
||||
-- Each item's name, icon and quality colour are the client's (the Item
|
||||
-- mixin), once its data has come from the server.
|
||||
local function LoadItem(entry)
|
||||
local data = Item:CreateFromItemID(entry.itemID);
|
||||
data:ContinueOnItemLoad(function()
|
||||
entry.name = data:GetItemName();
|
||||
entry.icon = data:GetItemIcon();
|
||||
local r, g, b = data:GetItemQualityColorRGB();
|
||||
entry.quality = { r, g, b };
|
||||
end);
|
||||
end
|
||||
for _, item in ipairs(ITEMS) do
|
||||
LoadItem(item);
|
||||
end
|
||||
for _, gear in pairs(GEAR) do
|
||||
LoadItem(gear);
|
||||
end
|
||||
-- The staff's other face, for its slot while it is Anathema.
|
||||
GEAR.benediction.anathema = { itemID = GEAR.benediction.other }
|
||||
LoadItem(GEAR.benediction.anathema)
|
||||
|
||||
-- For the files that load after this one.
|
||||
BW.LAIR_MAP = LAIR_MAP
|
||||
BW.CENTER = CENTER
|
||||
BW.ROOM_RADIUS = ROOM_RADIUS
|
||||
BW.SPAWN_MIN_DISTANCE = SPAWN_MIN_DISTANCE
|
||||
BW.FAR_CLIP = FAR_CLIP
|
||||
BW.CAMERA_YAW = CAMERA_YAW
|
||||
BW.CAMERA_PITCH = CAMERA_PITCH
|
||||
BW.CAMERA_ZOOM = CAMERA_ZOOM
|
||||
BW.RUN_SECONDS = RUN_SECONDS
|
||||
BW.ELITE_EVERY = ELITE_EVERY
|
||||
BW.MAX_ALIVE = MAX_ALIVE
|
||||
BW.RETARGET_SECONDS = RETARGET_SECONDS
|
||||
BW.SURROUND_RADIUS = SURROUND_RADIUS
|
||||
BW.CAST_SPACING = CAST_SPACING
|
||||
BW.DEATH_SECONDS = DEATH_SECONDS
|
||||
BW.ANIM_STAND = ANIM_STAND
|
||||
BW.ANIM_DEATH = ANIM_DEATH
|
||||
BW.ANIM_ATTACK_UNARMED = ANIM_ATTACK_UNARMED
|
||||
BW.HERO_HP = HERO_HP
|
||||
BW.MAGNET = MAGNET
|
||||
BW.GEM_SPEED = GEM_SPEED
|
||||
BW.GEM_TAKE = GEM_TAKE
|
||||
BW.GEM_PATH = GEM_PATH
|
||||
BW.MAX_GEMS = MAX_GEMS
|
||||
BW.KINDS = KINDS
|
||||
BW.ELITE = ELITE
|
||||
BW.EGG = EGG
|
||||
BW.SPELLS = SPELLS
|
||||
BW.SPELL_ORDER = SPELL_ORDER
|
||||
BW.EVOLUTIONS = EVOLUTIONS
|
||||
BW.FUSIONS = FUSIONS
|
||||
BW.MAX_SPELLS = MAX_SPELLS
|
||||
BW.EARLY_SPELLS = EARLY_SPELLS
|
||||
BW.STARTERS = STARTERS
|
||||
BW.PASSIVES = PASSIVES
|
||||
BW.SchoolColor = SchoolColor
|
||||
BW.HEAL_ICON = HEAL_ICON
|
||||
BW.UNKNOWN_ICON = UNKNOWN_ICON
|
||||
BW.PASSIVE_ORDER = PASSIVE_ORDER
|
||||
BW.VAMPIRIC_SHARE = VAMPIRIC_SHARE
|
||||
BW.HOLY_NOVA_HEAL_TARGETS = HOLY_NOVA_HEAL_TARGETS
|
||||
BW.SOULSTONE_SPELL = SOULSTONE_SPELL
|
||||
BW.SOULSTONE_BLAST = SOULSTONE_BLAST
|
||||
BW.MAX_LEVEL = MAX_LEVEL
|
||||
BW.ROMAN = ROMAN
|
||||
BW.EASE = EASE
|
||||
BW.AT_ONCE = AT_ONCE
|
||||
BW.TUNING = TUNING
|
||||
BW.ITEMS = ITEMS
|
||||
BW.GEAR = GEAR
|
||||
BW.GEAR_ORDER = GEAR_ORDER
|
||||
BW.MAX_WORN = MAX_WORN
|
||||
BW.CHEST_SOUND = CHEST_SOUND
|
||||
BW.GOLDEN_EVERY = GOLDEN_EVERY
|
||||
BW.CHESTS = CHESTS
|
||||
BW.VENT_PATH = VENT_PATH
|
||||
BW.VENT_MIRROR_PATH = VENT_MIRROR_PATH
|
||||
BW.VENTS = VENTS
|
||||
@@ -0,0 +1,83 @@
|
||||
-- Broodwatch eggs: a clutch laid in the lair hatches into whelps unless you
|
||||
-- break it first.
|
||||
|
||||
local _, BW = ...
|
||||
local EGG = BW.EGG
|
||||
local TUNING = BW.TUNING
|
||||
local Spawn = BW.Spawn
|
||||
local SpawnPoint = BW.SpawnPoint
|
||||
local Retire = BW.Retire
|
||||
local PickKind = BW.PickKind
|
||||
local FloorAt = BW.FloorAt
|
||||
local CENTER = BW.CENTER
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- An egg is an enemy (BW.enemies), so every spell can break it, and a broken
|
||||
-- one drops its gem as a whelp does. Here each egg counts down to its hatch:
|
||||
-- EGG_WARN seconds before, a red ring marks it, and at the hatch it cracks
|
||||
-- and EGG_WHELPS whelps (of the kinds spawning by then) come out where it stood.
|
||||
|
||||
local eggs = {} -- the eggs laid and not yet broken or hatched
|
||||
|
||||
-- Each egg knows its clutch: where it was laid and when it hatches (the
|
||||
-- off-screen markers show a clutch once).
|
||||
local function LayClutch()
|
||||
local cx, cy = SpawnPoint();
|
||||
local clutch = { x = cx, y = cy, z = FloorAt(cx, cy, CENTER[3]), hatchAt = BW.run.time + TUNING.EGG_HATCH };
|
||||
local n = TUNING.EGG_CLUTCH;
|
||||
for k = 1, n do
|
||||
local a = (k - 1) * 2 * math.pi / n + math.random() * 0.6;
|
||||
local e = Spawn(EGG, cx + TUNING.EGG_SPREAD * math.cos(a), cy + TUNING.EGG_SPREAD * math.sin(a));
|
||||
e.hatchAt = clutch.hatchAt;
|
||||
e.clutch = clutch;
|
||||
e.warned = nil;
|
||||
tinsert(eggs, e);
|
||||
end
|
||||
end
|
||||
|
||||
local function Hatch(e)
|
||||
e.actor:ClearGroundReticle();
|
||||
local x, y = e.actor:GetPosition();
|
||||
-- A dot on the egg ends with it: Corruption spreads only from a kill.
|
||||
e.dotLeft = nil;
|
||||
Retire(e);
|
||||
for _ = 1, TUNING.EGG_WHELPS do
|
||||
local a = math.random() * 2 * math.pi;
|
||||
Spawn(PickKind(), x + 0.6 * math.cos(a), y + 0.6 * math.sin(a));
|
||||
end
|
||||
end
|
||||
|
||||
local function UpdateEggs()
|
||||
if BW.run.time >= BW.run.nextClutch then
|
||||
BW.run.nextClutch = BW.run.nextClutch + TUNING.EGG_EVERY;
|
||||
LayClutch();
|
||||
end
|
||||
for i = table.getn(eggs), 1, -1 do
|
||||
local e = eggs[i];
|
||||
if not e.alive then
|
||||
-- Broken.
|
||||
e.actor:ClearGroundReticle();
|
||||
table.remove(eggs, i);
|
||||
elseif BW.run.time >= e.hatchAt then
|
||||
Hatch(e);
|
||||
table.remove(eggs, i);
|
||||
elseif not e.warned and BW.run.time >= e.hatchAt - TUNING.EGG_WARN then
|
||||
e.warned = true;
|
||||
e.actor:SetGroundReticle(0, false);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- The eggs themselves are enemies: ClearEnemies releases them.
|
||||
local function ClearEggs()
|
||||
for i = table.getn(eggs), 1, -1 do
|
||||
eggs[i].actor:ClearGroundReticle();
|
||||
eggs[i] = nil;
|
||||
end
|
||||
end
|
||||
|
||||
-- For the files that load after this one.
|
||||
BW.eggs = eggs
|
||||
BW.LayClutch = LayClutch
|
||||
BW.UpdateEggs = UpdateEggs
|
||||
BW.ClearEggs = ClearEggs
|
||||
@@ -0,0 +1,335 @@
|
||||
-- Broodwatch foes: what the lair's brood does besides bite. A kind with an
|
||||
-- `act` (Data.lua) stops, shows on the floor where it will strike, and
|
||||
-- strikes: a Warder throws fire at a spot, a drake charges along a line,
|
||||
-- a red whelp breathes a cone. You see it coming; you step out of it.
|
||||
|
||||
local _, BW = ...
|
||||
local TUNING = BW.TUNING
|
||||
local PASSIVES = BW.PASSIVES
|
||||
local CENTER = BW.CENTER
|
||||
local ROOM_RADIUS = BW.ROOM_RADIUS
|
||||
local enemies = BW.enemies
|
||||
local Dist2 = BW.Dist2
|
||||
local FloorAt = BW.FloorAt
|
||||
local ApplySpeed = BW.ApplySpeed
|
||||
local BiteShare = BW.BiteShare
|
||||
local Taunted = BW.Taunted
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Marks: empty actors that show a red reticle (an actor with no model shows
|
||||
-- its reticle). A strike's marks go when it lands, or when it is broken off.
|
||||
|
||||
local markPool = {}
|
||||
local marks = {} -- the marks shown, for the end of a run
|
||||
|
||||
local function Mark(x, y, z, radius)
|
||||
local mark = table.remove(markPool) or { actor = BW.scene:AcquireActor() };
|
||||
mark.actor:SetPosition(x, y, FloorAt(x, y, z));
|
||||
mark.actor:SetGroundReticle(radius, false);
|
||||
mark.actor:Show();
|
||||
marks[mark] = true;
|
||||
return mark;
|
||||
end
|
||||
|
||||
local function Unmark(list)
|
||||
for i = table.getn(list), 1, -1 do
|
||||
local mark = list[i];
|
||||
list[i] = nil;
|
||||
if marks[mark] then
|
||||
marks[mark] = nil;
|
||||
mark.actor:ClearGroundReticle();
|
||||
mark.actor:Hide();
|
||||
tinsert(markPool, mark);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- The hero is struck: `act.hit` seconds of the striker's bite, softened as a
|
||||
-- bite is (Demoralizing Shout's weakness, Devotion), of the act's school
|
||||
-- (Core.lua's HurtHero).
|
||||
|
||||
local function Strike(e, act)
|
||||
local devotion = 1 - PASSIVES.devotion.per * (BW.run.passives.devotion or 0) / 100;
|
||||
BW.HurtHero(e.dps * act.hit * BiteShare(e) * devotion, act.school, act.kind == "breath");
|
||||
end
|
||||
|
||||
-- Throws the hero, as a knockback spell does (not in the grace after the
|
||||
-- cards, nor while a Limited Invulnerability Potion lasts). The hero stops
|
||||
-- when it lands, so Steer's next move is sent again (a move given in the air
|
||||
-- takes effect on landing).
|
||||
local function KnockHero(fromX, fromY, knock)
|
||||
if BW.run.graceUntil or BW.run.invulnUntil then
|
||||
return;
|
||||
end
|
||||
if BW.hero:KnockBack(fromX, fromY, knock[1], knock[2]) then
|
||||
BW.moveFwd, BW.moveSide = 0, 0;
|
||||
end
|
||||
end
|
||||
|
||||
-- Whether the enemy can't act: held, fleeing, a sheep, drawn off by a
|
||||
-- taunt, or thrown (a knockback stops what it prepares).
|
||||
local function Unable(e)
|
||||
local t = BW.run.time;
|
||||
return (e.stunUntil and t < e.stunUntil) or e.fearUntil or e.sheepUntil or Taunted(e) or e.actor:IsFalling();
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- The acts. Each has Start(e, act, hx, hy, hz) (the wind-up: it shows its
|
||||
-- marks), and may have Charge(e, act, dt, hx, hy) (a move after the wind-up,
|
||||
-- until it returns true). A strike that comes with a spell's visual lands at
|
||||
-- the visual's impact (BW.OnImpact runs the in-flight entry's onImpact).
|
||||
|
||||
local acts = {}
|
||||
|
||||
local function Face(e, x, y)
|
||||
local ex, ey = e.actor:GetPosition();
|
||||
local yaw = math.atan2(y - ey, x - ex);
|
||||
e.actor:StopMoving();
|
||||
e.actor:SetYaw(yaw);
|
||||
return yaw, ex, ey;
|
||||
end
|
||||
|
||||
-- Fire thrown at the spot where you stand: the spot is marked through the
|
||||
-- cast and the missile's flight, and burns whoever is on it at the impact.
|
||||
acts.bolt = {
|
||||
Start = function(e, act, hx, hy, hz)
|
||||
Face(e, hx, hy);
|
||||
local z = FloorAt(hx, hy, hz);
|
||||
local castID = e.actor:PlaySpellVisualAtPoint(act.spell, hx, hy, z, act.cast * 1000);
|
||||
if not castID then
|
||||
return false;
|
||||
end
|
||||
local entry = { marks = { Mark(hx, hy, z, act.radius) }, x = hx, y = hy, e = e, act = act,
|
||||
expires = BW.run.time + act.cast + 3 };
|
||||
entry.onImpact = function(landed)
|
||||
Unmark(landed.marks);
|
||||
local px, py = BW.hero:GetPosition();
|
||||
if Dist2(px, py, landed.x, landed.y) <= landed.act.radius * landed.act.radius then
|
||||
Strike(landed.e, landed.act);
|
||||
end
|
||||
end;
|
||||
BW.run.inFlight[castID] = entry;
|
||||
e.actCast = castID;
|
||||
return true;
|
||||
end,
|
||||
}
|
||||
|
||||
-- A breath of fire in a cone ahead, `length` long and `arc` wide; three
|
||||
-- marks along it, each as wide as the cone where it lies.
|
||||
acts.breath = {
|
||||
Start = function(e, act, hx, hy, hz)
|
||||
local yaw, ex, ey = Face(e, hx, hy);
|
||||
local fx, fy = ex + act.length * math.cos(yaw), ey + act.length * math.sin(yaw);
|
||||
local castID = e.actor:PlaySpellVisualAtPoint(act.spell, fx, fy, hz, act.cast * 1000);
|
||||
if not castID then
|
||||
return false;
|
||||
end
|
||||
local entry = { marks = {}, x = ex, y = ey, yaw = yaw, e = e, act = act, expires = BW.run.time + act.cast + 3 };
|
||||
local half = math.tan(act.arc / 2);
|
||||
for k = 1, 3 do
|
||||
local d = act.length * (k - 0.5) / 3;
|
||||
tinsert(entry.marks, Mark(ex + d * math.cos(yaw), ey + d * math.sin(yaw), hz, d * half));
|
||||
end
|
||||
entry.onImpact = function(landed)
|
||||
Unmark(landed.marks);
|
||||
local px, py = BW.hero:GetPosition();
|
||||
local dx, dy = px - landed.x, py - landed.y;
|
||||
if dx * dx + dy * dy <= landed.act.length * landed.act.length then
|
||||
local off = math.mod(math.atan2(dy, dx) - landed.yaw + 3 * math.pi, 2 * math.pi) - math.pi;
|
||||
if math.abs(off) <= landed.act.arc / 2 then
|
||||
Strike(landed.e, landed.act);
|
||||
end
|
||||
end
|
||||
end;
|
||||
BW.run.inFlight[castID] = entry;
|
||||
e.actCast = castID;
|
||||
return true;
|
||||
end,
|
||||
}
|
||||
|
||||
-- A charge along a line through where you stand, `past` yards beyond you
|
||||
-- (short of the lair's wall: a move goes through walls): marks every
|
||||
-- MARK_STEP yards show it. After the wind-up it runs the line at
|
||||
-- `speed` (Go returns the most the run takes), strikes and throws you once
|
||||
-- if it passes within `width`, and stands dazed for `recover` at the end.
|
||||
local MARK_STEP = 2.5
|
||||
local CHARGE_ROOM = ROOM_RADIUS + 4 -- yards from CENTER a charge may end
|
||||
|
||||
local function InRoom(x, y)
|
||||
return Dist2(x, y, CENTER[1], CENTER[2]) <= CHARGE_ROOM * CHARGE_ROOM;
|
||||
end
|
||||
|
||||
acts.charge = {
|
||||
Start = function(e, act, hx, hy, hz)
|
||||
local yaw, ex, ey = Face(e, hx, hy);
|
||||
local length = math.min(math.sqrt(Dist2(ex, ey, hx, hy)) + act.past, act.range + act.past);
|
||||
while length > MARK_STEP and not InRoom(ex + length * math.cos(yaw), ey + length * math.sin(yaw)) do
|
||||
length = length - 1;
|
||||
end
|
||||
e.actX, e.actY, e.actZ = ex + length * math.cos(yaw), ey + length * math.sin(yaw), hz;
|
||||
e.actRun = length / act.speed + 0.5; -- the most the run takes (a wall can hold it)
|
||||
e.actMarks = e.actMarks or {};
|
||||
for d = MARK_STEP, length, MARK_STEP do
|
||||
tinsert(e.actMarks, Mark(ex + d * math.cos(yaw), ey + d * math.sin(yaw), hz, act.width));
|
||||
end
|
||||
return true;
|
||||
end,
|
||||
Go = function(e, act)
|
||||
e.actor:SetMovementSpeed("run", act.speed);
|
||||
e.actor:MoveTo(e.actX, e.actY, e.actZ);
|
||||
e.actor:PlaySpellVisual(act.spell, BW.hero, 0);
|
||||
e.actHit = nil;
|
||||
return e.actRun;
|
||||
end,
|
||||
Charge = function(e, act, dt, hx, hy)
|
||||
local x, y = e.actor:GetPosition();
|
||||
if not e.actHit and Dist2(x, y, hx, hy) <= act.width * act.width then
|
||||
e.actHit = true;
|
||||
Strike(e, act);
|
||||
KnockHero(x, y, act.knock);
|
||||
end
|
||||
return Dist2(x, y, e.actX, e.actY) <= 1 or e.actLeft <= 0;
|
||||
end,
|
||||
}
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- An act's states on the enemy: e.actState is "wind" (it stands and shows
|
||||
-- its marks, `cast` seconds), "go" (a charge's run), then "rest" (`recover`
|
||||
-- seconds; a thrown spell is out of its hands by then) and nil. e.actNext:
|
||||
-- the run time it may start again. A hold, a fear, a sheep, a taunt or a
|
||||
-- throw breaks a wind-up or a run off; what is already thrown still lands.
|
||||
|
||||
local function Next(e, act, share)
|
||||
e.actNext = BW.run.time + act.every * share;
|
||||
end
|
||||
|
||||
local function EndRun(e)
|
||||
ApplySpeed(e);
|
||||
e.actor:StopMoving();
|
||||
end
|
||||
|
||||
local function Break(e)
|
||||
local act = e.kind.act;
|
||||
if e.actState == "wind" and e.actCast then
|
||||
e.actor:StopSpellVisual(e.actCast);
|
||||
local entry = BW.run.inFlight[e.actCast];
|
||||
BW.run.inFlight[e.actCast] = nil;
|
||||
if entry then
|
||||
Unmark(entry.marks);
|
||||
end
|
||||
elseif e.actState == "go" then
|
||||
EndRun(e);
|
||||
end
|
||||
if e.actMarks then
|
||||
Unmark(e.actMarks);
|
||||
end
|
||||
e.actState, e.actCast = nil, nil;
|
||||
Next(e, act, 0.5);
|
||||
end
|
||||
|
||||
-- Whether a fresh enemy of this kind may start: in range of the hero (and,
|
||||
-- with `near`, not closer than it).
|
||||
local function InRange(e, act, hx, hy)
|
||||
local x, y = e.actor:GetPosition();
|
||||
local d2 = Dist2(x, y, hx, hy);
|
||||
return d2 <= act.range * act.range and (not act.near or d2 >= act.near * act.near);
|
||||
end
|
||||
|
||||
local function UpdateFoe(e, act, dt, hx, hy, hz)
|
||||
local state = e.actState;
|
||||
if not e.alive or Unable(e) then
|
||||
if state then
|
||||
Break(e);
|
||||
end
|
||||
return;
|
||||
end
|
||||
if not state then
|
||||
if not e.actNext then
|
||||
Next(e, act, 0.5 + math.random()); -- a fresh one waits a while, not in step with the rest
|
||||
elseif BW.run.time >= e.actNext and InRange(e, act, hx, hy)
|
||||
and acts[act.kind].Start(e, act, hx, hy, hz) then
|
||||
e.actState, e.actLeft = "wind", act.cast;
|
||||
end
|
||||
return;
|
||||
end
|
||||
e.actLeft = e.actLeft - dt;
|
||||
if state == "wind" then
|
||||
if e.actLeft <= 0 then
|
||||
if acts[act.kind].Go then
|
||||
e.actState, e.actLeft = "go", acts[act.kind].Go(e, act);
|
||||
else
|
||||
e.actState, e.actLeft, e.actCast = "rest", act.recover, nil;
|
||||
end
|
||||
end
|
||||
elseif state == "go" then
|
||||
if acts[act.kind].Charge(e, act, dt, hx, hy) then
|
||||
EndRun(e);
|
||||
Unmark(e.actMarks);
|
||||
e.actState, e.actLeft = "rest", act.recover;
|
||||
end
|
||||
elseif e.actLeft <= 0 then
|
||||
e.actState = nil;
|
||||
Next(e, act, 1);
|
||||
end
|
||||
end
|
||||
|
||||
-- Thrown fire and breath that never landed (its impact lost) clear their
|
||||
-- marks in time.
|
||||
local function Expire()
|
||||
for castID, entry in pairs(BW.run.inFlight) do
|
||||
if entry.expires and BW.run.time >= entry.expires then
|
||||
BW.run.inFlight[castID] = nil; -- clearing a key during `pairs` is allowed
|
||||
Unmark(entry.marks);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function UpdateFoes(dt, hx, hy, hz)
|
||||
for _, e in ipairs(enemies) do
|
||||
local act = e.kind.act;
|
||||
if act and (e.alive or e.actState) then
|
||||
UpdateFoe(e, act, dt, hx, hy, hz);
|
||||
end
|
||||
end
|
||||
Expire();
|
||||
end
|
||||
|
||||
-- Whether the enemy is busy with an act (Chase leaves it be).
|
||||
local function Acting(e)
|
||||
return e.actState ~= nil;
|
||||
end
|
||||
|
||||
-- Where a kind that keeps its distance (`keep`) goes: the point `keep` yards
|
||||
-- from the hero on its side, kept in the room.
|
||||
local function KeepPoint(e, hx, hy)
|
||||
local x, y = e.actor:GetPosition();
|
||||
local dx, dy = x - hx, y - hy;
|
||||
local d = math.sqrt(dx * dx + dy * dy);
|
||||
if d < 0.1 then
|
||||
dx, dy, d = math.cos(e.angle), math.sin(e.angle), 1;
|
||||
end
|
||||
local keep = e.kind.keep;
|
||||
local px, py = hx + dx / d * keep, hy + dy / d * keep;
|
||||
local cx, cy = px - CENTER[1], py - CENTER[2];
|
||||
local c = math.sqrt(cx * cx + cy * cy);
|
||||
if c > ROOM_RADIUS then
|
||||
px, py = CENTER[1] + cx / c * ROOM_RADIUS, CENTER[2] + cy / c * ROOM_RADIUS;
|
||||
end
|
||||
return px, py;
|
||||
end
|
||||
|
||||
local function ClearFoes()
|
||||
for mark in pairs(marks) do
|
||||
marks[mark] = nil;
|
||||
mark.actor:ClearGroundReticle();
|
||||
mark.actor:Hide();
|
||||
tinsert(markPool, mark);
|
||||
end
|
||||
end
|
||||
|
||||
-- For the files that load after this one.
|
||||
BW.UpdateFoes = UpdateFoes
|
||||
BW.Acting = Acting
|
||||
BW.KeepPoint = KeepPoint
|
||||
BW.ClearFoes = ClearFoes
|
||||
@@ -0,0 +1,320 @@
|
||||
-- Broodwatch gear: the weapon you hold and the gear you wear, kept for the
|
||||
-- run (Data.lua's GEAR). Chests offer it (Hud.lua).
|
||||
|
||||
local _, BW = ...
|
||||
local GEAR = BW.GEAR
|
||||
local enemies = BW.enemies
|
||||
local Dist2 = BW.Dist2
|
||||
local RunGrowth = BW.RunGrowth
|
||||
local SpellDamage = BW.SpellDamage
|
||||
local Burn = BW.Burn
|
||||
local Blast = BW.Blast
|
||||
local Slow = BW.Slow
|
||||
local Nearest = BW.Nearest
|
||||
local SchoolColor = BW.SchoolColor
|
||||
|
||||
local HAND_SLOTS = { 16, 17, 18 } -- main hand, off hand, ranged
|
||||
|
||||
-- What a weapon's own hits read in SpellDamage: their school (the combos
|
||||
-- and the colour) and that a gear proc does not proc again.
|
||||
local function ProcDef(gear)
|
||||
return { school = gear.school, hitColor = SchoolColor(gear.school), gear = true };
|
||||
end
|
||||
local THUNDERFURY = ProcDef(GEAR.thunderfury)
|
||||
local SULFURAS = ProcDef(GEAR.sulfuras)
|
||||
local RHOKDELAR = ProcDef(GEAR.rhokdelar)
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Taking it
|
||||
|
||||
local function Equip(key)
|
||||
local gear = GEAR[key];
|
||||
if gear.slot == "weapon" then
|
||||
BW.run.weapon = key;
|
||||
BW.run.procReady = 0;
|
||||
BW.run.shotLeft = 0;
|
||||
BW.hero:TryOn(gear.itemID);
|
||||
if key == "benediction" then
|
||||
BW.run.blessed = true;
|
||||
BW.run.turnAt = BW.run.time + gear.every;
|
||||
end
|
||||
else
|
||||
BW.run.gear[key] = true;
|
||||
if gear.every then
|
||||
BW.run.gearReady[key] = BW.run.time; -- ready at once
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- A new run: you hold your own weapons again.
|
||||
local function ResetGear()
|
||||
for _, slot in ipairs(HAND_SLOTS) do
|
||||
BW.hero:DressPlayerSlot(slot);
|
||||
end
|
||||
end
|
||||
|
||||
-- The gear a chest may offer: a weapon while you hold none (`weapon`), else
|
||||
-- worn gear you lack while you wear fewer than MAX_WORN.
|
||||
local function GearOffers(weapon)
|
||||
local list = {};
|
||||
local worn = 0;
|
||||
for _ in pairs(BW.run.gear) do
|
||||
worn = worn + 1;
|
||||
end
|
||||
for _, key in ipairs(BW.GEAR_ORDER) do
|
||||
local gear = GEAR[key];
|
||||
if weapon then
|
||||
if gear.slot == "weapon" and not BW.run.weapon then
|
||||
tinsert(list, key);
|
||||
end
|
||||
elseif gear.slot == "worn" and not BW.run.gear[key] and worn < BW.MAX_WORN then
|
||||
tinsert(list, key);
|
||||
end
|
||||
end
|
||||
return list;
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- What it does to your spells
|
||||
|
||||
-- The share of a spell's damage it deals: Benediction's or Anathema's
|
||||
-- school, and the on-use trinkets.
|
||||
local function GearScale(def)
|
||||
local run = BW.run;
|
||||
local scale = 1;
|
||||
if run.weapon == "benediction" then
|
||||
if (run.blessed and def.school == "Holy") or (not run.blessed and def.school == "Shadow") then
|
||||
scale = scale * (1 + GEAR.benediction.bonus);
|
||||
end
|
||||
end
|
||||
if run.tepUntil then
|
||||
scale = scale * (1 + GEAR.tep.bonus);
|
||||
end
|
||||
if run.zhcUntil then
|
||||
scale = scale * (1 + run.zhcBonus);
|
||||
end
|
||||
return scale;
|
||||
end
|
||||
|
||||
-- A spell has just been cast, to recharge in `cooldown` seconds: Hand of
|
||||
-- Justice may make it ready at once; the Hero Charm's power fades a step.
|
||||
local function AfterCast(cooldown)
|
||||
local run = BW.run;
|
||||
if run.zhcUntil then
|
||||
run.zhcBonus = math.max(0, run.zhcBonus - GEAR.zhc.fade);
|
||||
end
|
||||
if run.gear.hoj and math.random() < GEAR.hoj.chance then
|
||||
return 0;
|
||||
end
|
||||
return cooldown;
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- The weapons' procs: a spell hit (SpellDamage) may set one off.
|
||||
|
||||
-- Thunderfury's bolt: it hurts and slows its target, then leaps on to the
|
||||
-- nearest of the brood it has not hit, as Chain Lightning does.
|
||||
local leapHit
|
||||
local function WasHit(e)
|
||||
return leapHit[e];
|
||||
end
|
||||
|
||||
local function Thunder(entry)
|
||||
local gear = GEAR.thunderfury;
|
||||
local target = entry.target;
|
||||
entry.hit[target] = true;
|
||||
if target.alive then
|
||||
SpellDamage(target, entry.damage, THUNDERFURY);
|
||||
if target.alive then
|
||||
Slow(target, gear.slow, gear.slowSeconds);
|
||||
end
|
||||
end
|
||||
if entry.jumps <= 0 then
|
||||
return;
|
||||
end
|
||||
local x, y = target.actor:GetPosition();
|
||||
leapHit = entry.hit;
|
||||
local nextTarget = Nearest(x, y, gear.hop, WasHit);
|
||||
leapHit = nil;
|
||||
if nextTarget then
|
||||
local castID = target.actor:PlaySpellVisual(gear.spell, nextTarget.actor, 0);
|
||||
if castID then
|
||||
BW.run.inFlight[castID] = { target = nextTarget, damage = entry.damage, jumps = entry.jumps - 1,
|
||||
hit = entry.hit, onImpact = Thunder };
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Sulfuras's fireball bursts where it lands: it hurts, burns and throws the
|
||||
-- brood around.
|
||||
local function Inferno(entry)
|
||||
local gear = GEAR.sulfuras;
|
||||
local x, y = entry.target.actor:GetPosition();
|
||||
local r2 = gear.radius * gear.radius;
|
||||
local growth = RunGrowth();
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive then
|
||||
local ex, ey = e.actor:GetPosition();
|
||||
if Dist2(ex, ey, x, y) <= r2 then
|
||||
SpellDamage(e, gear.damage * growth, SULFURAS);
|
||||
if e.alive then
|
||||
Burn(e, gear.burn * growth, gear.burnSeconds, gear.school);
|
||||
end
|
||||
Blast(e, x, y, gear.knock);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local PROCS = {
|
||||
thunderfury = function(e, gear)
|
||||
local castID = BW.hero:PlaySpellVisual(gear.spell, e.actor, 0);
|
||||
local entry = { target = e, damage = gear.damage * RunGrowth(), jumps = gear.jumps, hit = {}, onImpact = Thunder };
|
||||
if castID then
|
||||
BW.run.inFlight[castID] = entry;
|
||||
else
|
||||
Thunder(entry);
|
||||
end
|
||||
end,
|
||||
sulfuras = function(e, gear)
|
||||
local castID = BW.hero:PlaySpellVisual(gear.spell, e.actor, 0);
|
||||
local entry = { target = e, onImpact = Inferno };
|
||||
if castID then
|
||||
BW.run.inFlight[castID] = entry;
|
||||
else
|
||||
Inferno(entry);
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
local function OnSpellHit(e, def)
|
||||
local run = BW.run;
|
||||
local proc = run.weapon and PROCS[run.weapon];
|
||||
if not proc or def.gear or run.time < run.procReady or not e.alive then
|
||||
return;
|
||||
end
|
||||
local gear = GEAR[run.weapon];
|
||||
if math.random() < gear.chance then
|
||||
run.procReady = run.time + gear.every;
|
||||
proc(e, gear);
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- The step
|
||||
|
||||
local function Shoot(entry)
|
||||
if entry.target.alive then
|
||||
SpellDamage(entry.target, entry.damage, RHOKDELAR);
|
||||
end
|
||||
end
|
||||
|
||||
local function NotAlive(e)
|
||||
return not e.alive;
|
||||
end
|
||||
|
||||
local function UpdateWeapon(dt, hx, hy)
|
||||
local run = BW.run;
|
||||
if run.weapon == "benediction" then
|
||||
local gear = GEAR.benediction;
|
||||
if run.time >= run.turnAt then
|
||||
run.turnAt = run.time + gear.every;
|
||||
run.blessed = not run.blessed;
|
||||
BW.hero:PlaySpellVisual(run.blessed and gear.toBenediction or gear.toAnathema, BW.hero, 0);
|
||||
BW.hero:TryOn(run.blessed and gear.itemID or gear.other);
|
||||
BW.UpdateBuild();
|
||||
end
|
||||
if run.blessed then
|
||||
run.pendingHeal = run.pendingHeal + gear.renew * dt;
|
||||
end
|
||||
elseif run.weapon == "rhokdelar" then
|
||||
local gear = GEAR.rhokdelar;
|
||||
run.shotLeft = run.shotLeft - dt;
|
||||
if run.shotLeft <= 0 then
|
||||
local target = Nearest(hx, hy, gear.range, NotAlive);
|
||||
if target then
|
||||
run.shotLeft = gear.every;
|
||||
local entry = { target = target, damage = gear.damage * RunGrowth(), onImpact = Shoot };
|
||||
local castID = BW.hero:PlaySpellVisual(gear.spell, target.actor, 0);
|
||||
if castID then
|
||||
run.inFlight[castID] = entry;
|
||||
else
|
||||
Shoot(entry);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Whether any of the brood is within `range` of the hero.
|
||||
local function BroodNear(hx, hy, range)
|
||||
local r2 = range * range;
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
if Dist2(x, y, hx, hy) <= r2 then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
end
|
||||
return false;
|
||||
end
|
||||
|
||||
-- The on-use trinkets go off when they are ready and the brood is near.
|
||||
local function UpdateTrinkets(hx, hy)
|
||||
local run = BW.run;
|
||||
local t = run.time;
|
||||
if run.tepUntil and t >= run.tepUntil then
|
||||
run.tepUntil = nil;
|
||||
end
|
||||
if run.zhcUntil and t >= run.zhcUntil then
|
||||
run.zhcUntil, run.zhcBonus = nil, nil;
|
||||
end
|
||||
for key, ready in pairs(run.gearReady) do
|
||||
local gear = GEAR[key];
|
||||
if t >= ready and BroodNear(hx, hy, gear.near) then
|
||||
run.gearReady[key] = t + gear.every;
|
||||
run[key .. "Until"] = t + gear.seconds;
|
||||
if key == "zhc" then
|
||||
run.zhcBonus = gear.bonus;
|
||||
end
|
||||
BW.hero:PlaySpellVisual(gear.spell, BW.hero, 0);
|
||||
local slot = run.gearSlots[key];
|
||||
if slot then
|
||||
slot.cd:SetCooldown(GetTime(), gear.every);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- The potions' times.
|
||||
local function UpdatePotions()
|
||||
local run = BW.run;
|
||||
local changed = false;
|
||||
if run.invulnUntil and run.time >= run.invulnUntil then
|
||||
run.invulnUntil = nil;
|
||||
changed = true;
|
||||
end
|
||||
if run.fireWard and run.time >= run.fireWardUntil then
|
||||
run.fireWard = nil;
|
||||
changed = true;
|
||||
end
|
||||
if changed then
|
||||
BW.UpdateHealth();
|
||||
end
|
||||
end
|
||||
|
||||
local function UpdateGear(dt, hx, hy)
|
||||
UpdateWeapon(dt, hx, hy);
|
||||
UpdateTrinkets(hx, hy);
|
||||
UpdatePotions();
|
||||
end
|
||||
|
||||
-- For the files that load after this one.
|
||||
BW.Equip = Equip
|
||||
BW.ResetGear = ResetGear
|
||||
BW.GearOffers = GearOffers
|
||||
BW.GearScale = GearScale
|
||||
BW.AfterCast = AfterCast
|
||||
BW.OnSpellHit = OnSpellHit
|
||||
BW.UpdateGear = UpdateGear
|
||||
@@ -0,0 +1,761 @@
|
||||
-- Broodwatch HUD: the level-up cards, the build bar, health and experience.
|
||||
|
||||
local _, BW = ...
|
||||
local HERO_HP = BW.HERO_HP
|
||||
local SPELLS = BW.SPELLS
|
||||
local SPELL_ORDER = BW.SPELL_ORDER
|
||||
local EVOLUTIONS = BW.EVOLUTIONS
|
||||
local FUSIONS = BW.FUSIONS
|
||||
local MAX_SPELLS = BW.MAX_SPELLS
|
||||
local EARLY_SPELLS = BW.EARLY_SPELLS
|
||||
local PASSIVES = BW.PASSIVES
|
||||
local HEAL_ICON = BW.HEAL_ICON
|
||||
local UNKNOWN_ICON = BW.UNKNOWN_ICON
|
||||
local PASSIVE_ORDER = BW.PASSIVE_ORDER
|
||||
local MAX_LEVEL = BW.MAX_LEVEL
|
||||
local ROMAN = BW.ROMAN
|
||||
local EASE = BW.EASE
|
||||
local AT_ONCE = BW.AT_ONCE
|
||||
local offers = BW.offers
|
||||
local Shuffle = BW.Shuffle
|
||||
local Stat = BW.Stat
|
||||
local TUNING = BW.TUNING
|
||||
local GEAR = BW.GEAR
|
||||
local GEAR_ORDER = BW.GEAR_ORDER
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Level-up offers
|
||||
|
||||
local function BuildOffers()
|
||||
-- The start: your class's first spell, then two of the other classes'.
|
||||
if BW.run.picking == "start" then
|
||||
local _, class = UnitClass("player");
|
||||
local own = BW.STARTERS[class];
|
||||
local others = {};
|
||||
for _, key in pairs(BW.STARTERS) do
|
||||
if key ~= own then
|
||||
tinsert(others, key);
|
||||
end
|
||||
end
|
||||
Shuffle(others);
|
||||
if own then
|
||||
tinsert(others, 1, own);
|
||||
end
|
||||
for i = 1, 3 do
|
||||
offers[i] = { kind = "spell", key = others[i], level = 1 };
|
||||
end
|
||||
return;
|
||||
end
|
||||
-- A golden chest's weapon pick: three of the weapons.
|
||||
if BW.run.picking == "chest" and BW.run.weaponPick then
|
||||
local weapons = BW.GearOffers(true);
|
||||
Shuffle(weapons);
|
||||
for i = 1, 3 do
|
||||
offers[i] = weapons[i] and { kind = "gear", key = weapons[i] } or { kind = "heal" };
|
||||
end
|
||||
return;
|
||||
end
|
||||
local all = {};
|
||||
local owned = 0;
|
||||
for _ in pairs(BW.run.spells) do
|
||||
owned = owned + 1;
|
||||
end
|
||||
for _, key in ipairs(SPELL_ORDER) do
|
||||
local state = BW.run.spells[key];
|
||||
if SPELLS[key].evolved then
|
||||
-- An evolution comes only from its spell (below), and has one rank.
|
||||
elseif state and state.level < MAX_LEVEL then
|
||||
tinsert(all, { kind = "spell", key = key, level = state.level + 1 });
|
||||
elseif not state and owned < MAX_SPELLS then
|
||||
tinsert(all, { kind = "spell", key = key, level = 1 });
|
||||
end
|
||||
end
|
||||
for _, key in ipairs(PASSIVE_ORDER) do
|
||||
local level = (BW.run.passives[key] or 0) + 1;
|
||||
if level <= (PASSIVES[key].max or MAX_LEVEL) then
|
||||
tinsert(all, { kind = "passive", key = key, level = level });
|
||||
end
|
||||
end
|
||||
Shuffle(all);
|
||||
-- An early build must not stall: the first level offers only new spells,
|
||||
-- and until you have EARLY_SPELLS spells each level offers at least one.
|
||||
local fresh = {};
|
||||
for _, o in ipairs(all) do
|
||||
if o.kind == "spell" and o.level == 1 then
|
||||
tinsert(fresh, o);
|
||||
end
|
||||
end
|
||||
if owned == 1 and table.getn(fresh) >= 3 then
|
||||
all = fresh;
|
||||
elseif owned < EARLY_SPELLS and table.getn(fresh) > 0 then
|
||||
local hasFresh = false;
|
||||
for i = 1, 3 do
|
||||
local o = all[i];
|
||||
if o and o.kind == "spell" and o.level == 1 then
|
||||
hasFresh = true;
|
||||
end
|
||||
end
|
||||
if not hasFresh then
|
||||
-- The new spell takes the third card's place.
|
||||
for i, o in ipairs(all) do
|
||||
if o == fresh[1] then
|
||||
table.remove(all, i);
|
||||
break;
|
||||
end
|
||||
end
|
||||
tinsert(all, 3, fresh[1]);
|
||||
end
|
||||
end
|
||||
-- A chest's pick may offer a piece of worn gear.
|
||||
if BW.run.picking == "chest" then
|
||||
local worn = BW.GearOffers(false);
|
||||
if worn[1] then
|
||||
tinsert(all, 1, { kind = "gear", key = worn[math.random(table.getn(worn))] });
|
||||
end
|
||||
end
|
||||
-- An evolution ready (its spell at rank V, its passive owned) always
|
||||
-- takes the first card.
|
||||
for _, key in ipairs(SPELL_ORDER) do
|
||||
local evolution = EVOLUTIONS[key];
|
||||
local state = BW.run.spells[key];
|
||||
if evolution and state and state.level >= MAX_LEVEL and BW.run.passives[evolution.needs] then
|
||||
tinsert(all, 1, { kind = "evolve", key = evolution.into, from = key, level = 1 });
|
||||
break;
|
||||
end
|
||||
end
|
||||
-- A fusion ready (both its spells at rank V) comes before that.
|
||||
for _, fusion in ipairs(FUSIONS) do
|
||||
local a, b = BW.run.spells[fusion.a], BW.run.spells[fusion.b];
|
||||
if a and b and a.level >= MAX_LEVEL and b.level >= MAX_LEVEL then
|
||||
tinsert(all, 1, { kind = "fuse", key = fusion.into, a = fusion.a, b = fusion.b, level = 1 });
|
||||
break;
|
||||
end
|
||||
end
|
||||
for i = 1, 3 do
|
||||
offers[i] = all[i] or { kind = "heal" };
|
||||
end
|
||||
end
|
||||
|
||||
-- Card colours by kind: a new spell gold, a spell's next rank blue, a
|
||||
-- passive green, the heal when nothing is left red.
|
||||
local CARD_COLORS = {
|
||||
new = { 1, 0.82, 0 },
|
||||
spell = { 0.35, 0.65, 1 },
|
||||
passive = { 0.3, 0.9, 0.4 },
|
||||
heal = { 1, 0.45, 0.45 },
|
||||
evolve = { 0.85, 0.5, 1 },
|
||||
fuse = { 1, 0.55, 0.15 },
|
||||
gear = { 0.64, 0.21, 0.93 }, -- epic, until the item's own quality colour loads
|
||||
}
|
||||
|
||||
-- A piece of gear's colour: its item quality's.
|
||||
local function GearColor(gear)
|
||||
return gear.quality or CARD_COLORS.gear;
|
||||
end
|
||||
local STAT_LABELS = {
|
||||
damage = "Damage", count = "Targets", jumps = "Jumps", totems = "Totems", spread = "Spreads to",
|
||||
heal = "Heal per hit", radius = "Radius", slow = "Enemy speed", duration = "Lasts", cooldown = "Every",
|
||||
absorb = "Absorbs", root = "Freezes for", fear = "Fears for", weaken = "Their bites",
|
||||
hit = "Infernal's blows", burn = "Immolation per second", orbs = "Orbs", selfBurn = "Burns you for",
|
||||
length = "Reaches", ignite = "Burns per second", traps = "Traps", freeze = "Freezes for", stun = "Stuns for",
|
||||
pulseDamage = "Pulse",
|
||||
}
|
||||
local STAT_UNITS = {
|
||||
radius = " yd", duration = " s", cooldown = " s", root = " s", fear = " s", length = " yd", freeze = " s",
|
||||
stun = " s",
|
||||
}
|
||||
local STAT_ORDER = {
|
||||
"damage", "hit", "pulseDamage", "burn", "ignite", "selfBurn", "orbs", "count", "jumps", "totems", "traps", "spread", "heal",
|
||||
"absorb", "radius", "length", "slow", "root", "freeze", "stun", "fear",
|
||||
"weaken",
|
||||
"duration", "cooldown",
|
||||
}
|
||||
|
||||
local function Num(v)
|
||||
if v == math.floor(v) then
|
||||
return string.format("%d", v);
|
||||
end
|
||||
local text = string.format("%.2f", v);
|
||||
if string.sub(text, -1) == "0" then
|
||||
text = string.sub(text, 1, -2);
|
||||
end
|
||||
return text;
|
||||
end
|
||||
|
||||
-- A stat as the cards show it: the slow as a percentage of speed, the rest
|
||||
-- with their unit.
|
||||
local function StatText(field, v)
|
||||
if field == "slow" then
|
||||
return math.floor(v * 100 + 0.5) .. "%";
|
||||
elseif field == "weaken" then
|
||||
return "-" .. math.floor(v * 100 + 0.5) .. "%";
|
||||
elseif field == "absorb" then
|
||||
return Num(v) .. "% of health";
|
||||
end
|
||||
return Num(v) .. (STAT_UNITS[field] or "");
|
||||
end
|
||||
|
||||
local function Icon(spellID)
|
||||
return C_Spell.GetSpellTexture(spellID) or UNKNOWN_ICON;
|
||||
end
|
||||
|
||||
-- The card's lines: a new spell's blurb and stats, a rank's changes (old in
|
||||
-- grey, new in green), a passive's rank and its total.
|
||||
local function OfferLines(o)
|
||||
local lines = {};
|
||||
if o.kind == "spell" then
|
||||
local def = SPELLS[o.key];
|
||||
if o.level == 1 then
|
||||
tinsert(lines, def.blurb);
|
||||
tinsert(lines, "");
|
||||
end
|
||||
for _, field in ipairs(STAT_ORDER) do
|
||||
local b = Stat(def, field, o.level);
|
||||
if b then
|
||||
if o.level == 1 then
|
||||
tinsert(lines, STAT_LABELS[field] .. " " .. StatText(field, b));
|
||||
else
|
||||
local a = Stat(def, field, o.level - 1);
|
||||
if a ~= b then
|
||||
tinsert(lines, string.format("%s |cff888888%s|r > |cff40ff40%s|r",
|
||||
STAT_LABELS[field], StatText(field, a), StatText(field, b)));
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-- The mastery: a teaser on a new spell's card, the prize at rank V.
|
||||
if def.mastery then
|
||||
if o.level == MAX_LEVEL then
|
||||
tinsert(lines, "");
|
||||
tinsert(lines, "|cffffd100Mastery:|r " .. def.mastery.text);
|
||||
elseif o.level == 1 then
|
||||
tinsert(lines, "");
|
||||
tinsert(lines, "|cff888888At rank V: " .. def.mastery.text .. "|r");
|
||||
end
|
||||
end
|
||||
local evolution = EVOLUTIONS[o.key];
|
||||
if evolution and o.level == MAX_LEVEL then
|
||||
tinsert(lines, "|cffd980ffEvolves with " .. PASSIVES[evolution.needs].name .. ".|r");
|
||||
end
|
||||
for _, fusion in ipairs(FUSIONS) do
|
||||
if o.level == MAX_LEVEL and (fusion.a == o.key or fusion.b == o.key) then
|
||||
local other = fusion.a == o.key and fusion.b or fusion.a;
|
||||
tinsert(lines, "|cffff8c26Fuses with " .. SPELLS[other].name .. " at rank V.|r");
|
||||
end
|
||||
end
|
||||
elseif o.kind == "evolve" or o.kind == "fuse" then
|
||||
local def = SPELLS[o.key];
|
||||
if o.kind == "fuse" then
|
||||
tinsert(lines, "Fuses " .. SPELLS[o.a].name .. " and " .. SPELLS[o.b].name .. ", and frees a slot.");
|
||||
else
|
||||
tinsert(lines, "Replaces " .. SPELLS[o.from].name .. ".");
|
||||
end
|
||||
tinsert(lines, "");
|
||||
tinsert(lines, def.blurb);
|
||||
tinsert(lines, "");
|
||||
for _, field in ipairs(STAT_ORDER) do
|
||||
local v = Stat(def, field, 1);
|
||||
if v then
|
||||
tinsert(lines, STAT_LABELS[field] .. " " .. StatText(field, v));
|
||||
end
|
||||
end
|
||||
elseif o.kind == "gear" then
|
||||
tinsert(lines, GEAR[o.key].text);
|
||||
tinsert(lines, "");
|
||||
tinsert(lines, "|cff888888Kept for the run.|r");
|
||||
elseif o.kind == "passive" then
|
||||
local p = PASSIVES[o.key];
|
||||
tinsert(lines, p.text);
|
||||
tinsert(lines, "");
|
||||
tinsert(lines, "|cff888888Total:|r " .. string.format(p.total, p.per * o.level));
|
||||
else
|
||||
tinsert(lines, "Heal 50 health.");
|
||||
end
|
||||
return table.concat(lines, "\n");
|
||||
end
|
||||
|
||||
-- A card grows to its text (a long name wraps too): the parts above the
|
||||
-- description (the icon at 20 from the top, 64 tall, the name 10 below it,
|
||||
-- the tag 4 below that, the description 14 below that) and below it (12
|
||||
-- above the pips, which sit 40 from the bottom, 6 tall), as Broodwatch.xml
|
||||
-- lays them out.
|
||||
local CARD_ABOVE = 20 + 64 + 10 + 4 + 14
|
||||
local CARD_BELOW = 12 + 6 + 40
|
||||
local CARD_MIN_HEIGHT = 280
|
||||
local CARD_DESC_ROOM = 1000 -- the description's height while it is measured
|
||||
|
||||
-- Fills card i with offer o; returns the height the card needs.
|
||||
local function FillCard(i, o)
|
||||
local card = getglobal(BW.cards:GetName() .. "Card" .. i);
|
||||
local name = card:GetName();
|
||||
local icon, title, tag, colorKey;
|
||||
if o.kind == "spell" then
|
||||
local def = SPELLS[o.key];
|
||||
icon, title = Icon(def.spell), def.name;
|
||||
if o.level == 1 then
|
||||
tag, colorKey = "New spell", "new";
|
||||
else
|
||||
tag, colorKey = "Rank " .. ROMAN[o.level], "spell";
|
||||
end
|
||||
elseif o.kind == "passive" then
|
||||
local p = PASSIVES[o.key];
|
||||
icon, title = Icon(p.icon), p.name;
|
||||
tag, colorKey = "Passive, rank " .. ROMAN[o.level], "passive";
|
||||
elseif o.kind == "evolve" then
|
||||
local def = SPELLS[o.key];
|
||||
icon, title, tag, colorKey = Icon(def.spell), def.name, "Evolution", "evolve";
|
||||
elseif o.kind == "fuse" then
|
||||
local def = SPELLS[o.key];
|
||||
icon, title, tag, colorKey = Icon(def.spell), def.name, "Fusion", "fuse";
|
||||
elseif o.kind == "gear" then
|
||||
local gear = GEAR[o.key];
|
||||
icon, title = gear.icon or UNKNOWN_ICON, gear.name or "";
|
||||
tag = gear.slot == "weapon" and "Weapon" or "Worn";
|
||||
else
|
||||
icon, title, tag, colorKey = Icon(HEAL_ICON), "Second Wind", "Everything is at rank V", "heal";
|
||||
end
|
||||
local color = o.kind == "gear" and GearColor(GEAR[o.key]) or CARD_COLORS[colorKey];
|
||||
card.color = color;
|
||||
card:SetBackdropColor(0.04, 0.04, 0.06, 0.95);
|
||||
card:SetBackdropBorderColor(color[1], color[2], color[3]);
|
||||
getglobal(name .. "Icon"):SetTexture(icon);
|
||||
local nameText = getglobal(name .. "Name");
|
||||
nameText:SetHeight(CARD_DESC_ROOM);
|
||||
nameText:SetText(title);
|
||||
nameText:SetHeight(nameText:GetStringHeight());
|
||||
nameText:SetTextColor(color[1], color[2], color[3]);
|
||||
local tagText = getglobal(name .. "Tag");
|
||||
tagText:SetText(tag);
|
||||
local desc = getglobal(name .. "Desc");
|
||||
desc:SetHeight(CARD_DESC_ROOM);
|
||||
desc:SetText(OfferLines(o));
|
||||
local descHeight = desc:GetStringHeight();
|
||||
desc:SetHeight(descHeight);
|
||||
getglobal(name .. "Key"):SetText("Press " .. i);
|
||||
-- The ranks: those you have in the card's colour, the new one white.
|
||||
local maxRank = o.kind == "passive" and PASSIVES[o.key].max or MAX_LEVEL;
|
||||
for r = 1, MAX_LEVEL do
|
||||
local pip = getglobal(name .. "Pip" .. r);
|
||||
if o.kind == "heal" or o.kind == "evolve" or o.kind == "fuse" or o.kind == "gear" or r > maxRank then
|
||||
pip:Hide();
|
||||
else
|
||||
pip:Show();
|
||||
if r < o.level then
|
||||
pip:SetVertexColor(color[1], color[2], color[3]);
|
||||
elseif r == o.level then
|
||||
pip:SetVertexColor(1, 1, 1);
|
||||
else
|
||||
pip:SetVertexColor(0.2, 0.2, 0.2);
|
||||
end
|
||||
end
|
||||
end
|
||||
return CARD_ABOVE + nameText:GetStringHeight() + tagText:GetStringHeight() + descHeight + CARD_BELOW;
|
||||
end
|
||||
|
||||
-- A build slot's tooltip: what the spell or passive does at the rank you
|
||||
-- have, or what the gear does.
|
||||
local function ShowSlotTip(slot)
|
||||
local tip = GameTooltip;
|
||||
tip:SetOwner(slot, "ANCHOR_RIGHT");
|
||||
if slot.gearKey then
|
||||
local gear = GEAR[slot.gearKey];
|
||||
local color = GearColor(gear);
|
||||
tip:SetText(gear.name or "", color[1], color[2], color[3]);
|
||||
if slot.gearKey == "benediction" and not BW.run.blessed then
|
||||
tip:AddLine(gear.anathema.name or "", 0.6, 0.6, 0.6);
|
||||
end
|
||||
tip:AddLine(gear.text, 1, 1, 1, 1);
|
||||
elseif slot.spellKey then
|
||||
local def = SPELLS[slot.spellKey];
|
||||
local level = BW.run.spells[slot.spellKey].level;
|
||||
local color = def.fused and CARD_COLORS.fuse or def.evolved and CARD_COLORS.evolve or CARD_COLORS.spell;
|
||||
tip:SetText(def.name, color[1], color[2], color[3]);
|
||||
if def.fused then
|
||||
for _, fusion in ipairs(FUSIONS) do
|
||||
if fusion.into == slot.spellKey then
|
||||
tip:AddLine("Fused from " .. SPELLS[fusion.a].name .. " and " .. SPELLS[fusion.b].name, 0.6, 0.6, 0.6, 1);
|
||||
end
|
||||
end
|
||||
elseif def.evolved then
|
||||
for from, evolution in pairs(EVOLUTIONS) do
|
||||
if evolution.into == slot.spellKey then
|
||||
tip:AddLine("Evolved from " .. SPELLS[from].name .. " and " .. PASSIVES[evolution.needs].name,
|
||||
0.6, 0.6, 0.6, 1);
|
||||
end
|
||||
end
|
||||
else
|
||||
tip:AddLine("Rank " .. ROMAN[level], 0.6, 0.6, 0.6);
|
||||
end
|
||||
tip:AddLine(def.blurb, 1, 1, 1, 1);
|
||||
for _, field in ipairs(STAT_ORDER) do
|
||||
local v = Stat(def, field, level);
|
||||
if v then
|
||||
tip:AddDoubleLine(STAT_LABELS[field], StatText(field, v), 0.8, 0.8, 0.8, 1, 1, 1);
|
||||
end
|
||||
end
|
||||
if def.mastery then
|
||||
if level == MAX_LEVEL then
|
||||
tip:AddLine("Mastery: " .. def.mastery.text, 1, 0.82, 0, 1);
|
||||
else
|
||||
tip:AddLine("At rank V: " .. def.mastery.text, 0.53, 0.53, 0.53, 1);
|
||||
end
|
||||
end
|
||||
local evolution = EVOLUTIONS[slot.spellKey];
|
||||
if evolution then
|
||||
local into = SPELLS[evolution.into];
|
||||
tip:AddLine("At rank V with " .. PASSIVES[evolution.needs].name .. ", evolves into " .. into.name .. ".",
|
||||
0.85, 0.5, 1, 1);
|
||||
end
|
||||
for _, fusion in ipairs(FUSIONS) do
|
||||
if fusion.a == slot.spellKey or fusion.b == slot.spellKey then
|
||||
local other = fusion.a == slot.spellKey and fusion.b or fusion.a;
|
||||
tip:AddLine("At rank V with " .. SPELLS[other].name .. " at rank V, fuses into "
|
||||
.. SPELLS[fusion.into].name .. ".", 1, 0.55, 0.15, 1);
|
||||
end
|
||||
end
|
||||
else
|
||||
local p = PASSIVES[slot.passiveKey];
|
||||
local level = BW.run.passives[slot.passiveKey];
|
||||
local color = CARD_COLORS.passive;
|
||||
tip:SetText(p.name, color[1], color[2], color[3]);
|
||||
tip:AddLine("Passive, rank " .. ROMAN[level], 0.6, 0.6, 0.6);
|
||||
tip:AddLine(p.text, 1, 1, 1, 1);
|
||||
tip:AddLine("Total: " .. string.format(p.total, p.per * level), 0.8, 0.8, 0.8, 1);
|
||||
end
|
||||
tip:Show();
|
||||
end
|
||||
|
||||
local function HideSlotTip()
|
||||
GameTooltip:Hide();
|
||||
end
|
||||
|
||||
function Broodwatch_CardHover(card, over)
|
||||
if over then
|
||||
card:SetBackdropBorderColor(1, 1, 1);
|
||||
elseif card.color then
|
||||
card:SetBackdropBorderColor(card.color[1], card.color[2], card.color[3]);
|
||||
end
|
||||
end
|
||||
|
||||
-- The build bar: an icon for each spell and passive you have, with its rank.
|
||||
local BUILD_SLOT = 32
|
||||
local buildSlots = {}
|
||||
|
||||
local function BuildSlot(i)
|
||||
local slot = buildSlots[i];
|
||||
if not slot then
|
||||
slot = CreateFrame("Frame", nil, BW.build);
|
||||
slot:SetWidth(BUILD_SLOT);
|
||||
slot:SetHeight(BUILD_SLOT);
|
||||
slot.icon = slot:CreateTexture(nil, "ARTWORK");
|
||||
slot.icon:SetAllPoints(slot);
|
||||
-- The spell's recharge sweeps over its icon. No flash at the end:
|
||||
-- the fast spells would flash all the time.
|
||||
slot.cd = CreateFrame("Cooldown", nil, slot);
|
||||
slot.cd:SetAllPoints(slot);
|
||||
slot.cd:SetDrawEdge(true);
|
||||
slot.cd:SetDrawBling(false);
|
||||
slot.cd:SetHideCountdownNumbers(true);
|
||||
slot.cd:SetPaused(BW.paused);
|
||||
-- The rank sits above the sweep.
|
||||
slot.top = CreateFrame("Frame", nil, slot);
|
||||
slot.top:SetAllPoints(slot);
|
||||
slot.top:SetFrameLevel(slot.cd:GetFrameLevel() + 1);
|
||||
slot.rank = slot.top:CreateFontString(nil, "OVERLAY");
|
||||
slot.rank:SetFontObject(NumberFontNormal);
|
||||
slot.rank:SetPoint("BOTTOMRIGHT", slot, "BOTTOMRIGHT", -1, 1);
|
||||
slot:EnableMouse(true);
|
||||
slot:SetScript("OnEnter", function() ShowSlotTip(slot); end);
|
||||
slot:SetScript("OnLeave", HideSlotTip);
|
||||
buildSlots[i] = slot;
|
||||
end
|
||||
return slot;
|
||||
end
|
||||
|
||||
local buildCount = 0
|
||||
|
||||
local PASSIVE_SLOT = 24
|
||||
|
||||
local function AddBuildSlot(icon, level, x, y, size)
|
||||
buildCount = buildCount + 1;
|
||||
local slot = BuildSlot(buildCount);
|
||||
slot:SetWidth(size);
|
||||
slot:SetHeight(size);
|
||||
slot:ClearAllPoints();
|
||||
slot:SetPoint("BOTTOMLEFT", BW.build, "BOTTOMLEFT", x, y);
|
||||
slot.icon:SetTexture(icon);
|
||||
slot.icon:SetDesaturated(false);
|
||||
slot.rank:SetText(ROMAN[level]);
|
||||
slot:Show();
|
||||
return slot;
|
||||
end
|
||||
|
||||
-- Shows a spell's recharge on its slot: the part of `full` seconds left in
|
||||
-- `left`, measured on the game's clock (the sweep pauses with the game).
|
||||
local function ShowRecharge(slot, left, full)
|
||||
if left > 0 and full and full > 0 then
|
||||
slot.cd:SetCooldown(GetTime() - (full - left), full);
|
||||
else
|
||||
slot.cd:Clear();
|
||||
end
|
||||
end
|
||||
|
||||
local function UpdateBuild()
|
||||
buildCount = 0;
|
||||
local x = 0;
|
||||
for _, key in ipairs(SPELL_ORDER) do
|
||||
local state = BW.run.spells[key];
|
||||
if state then
|
||||
-- A new spell can move the others along the bar: each slot shows
|
||||
-- its own spell's recharge again.
|
||||
state.slot = AddBuildSlot(Icon(SPELLS[key].spell), state.level, x, 0, BUILD_SLOT);
|
||||
state.slot.spellKey, state.slot.passiveKey, state.slot.gearKey = key, nil, nil;
|
||||
if SPELLS[key].evolved then
|
||||
state.slot.rank:SetText("|cffd980ffE|r");
|
||||
end
|
||||
ShowRecharge(state.slot, state.cooldown, state.cdFull);
|
||||
x = x + BUILD_SLOT + 4;
|
||||
end
|
||||
end
|
||||
x = 0;
|
||||
for _, key in ipairs(PASSIVE_ORDER) do
|
||||
local level = BW.run.passives[key];
|
||||
if level then
|
||||
local slot = AddBuildSlot(Icon(PASSIVES[key].icon), level, x, BUILD_SLOT + 6, PASSIVE_SLOT);
|
||||
slot.spellKey, slot.passiveKey, slot.gearKey = nil, key, nil;
|
||||
slot.cd:Clear();
|
||||
slot.icon:SetDesaturated(key == "soulstone" and BW.run.soulstoneUsed and true or false);
|
||||
x = x + PASSIVE_SLOT + 3;
|
||||
end
|
||||
end
|
||||
-- The gear, above the passives: the weapon first. An on-use trinket shows
|
||||
-- its recharge.
|
||||
x = 0;
|
||||
local gearY = BUILD_SLOT + 6 + PASSIVE_SLOT + 6;
|
||||
for _, key in ipairs(GEAR_ORDER) do
|
||||
if BW.run.weapon == key or BW.run.gear[key] then
|
||||
local gear = GEAR[key];
|
||||
local icon = gear.icon;
|
||||
if key == "benediction" and not BW.run.blessed then
|
||||
icon = gear.anathema.icon or icon;
|
||||
end
|
||||
local slot = AddBuildSlot(icon or UNKNOWN_ICON, 1, x, gearY, PASSIVE_SLOT);
|
||||
slot.spellKey, slot.passiveKey, slot.gearKey = nil, nil, key;
|
||||
slot.rank:SetText("");
|
||||
BW.run.gearSlots[key] = slot;
|
||||
local ready = BW.run.gearReady[key];
|
||||
if ready then
|
||||
ShowRecharge(slot, ready - BW.run.time, gear.every);
|
||||
else
|
||||
slot.cd:Clear();
|
||||
end
|
||||
x = x + PASSIVE_SLOT + 3;
|
||||
end
|
||||
end
|
||||
for i = buildCount + 1, table.getn(buildSlots) do
|
||||
buildSlots[i]:Hide();
|
||||
end
|
||||
end
|
||||
|
||||
local function SetPaused(on)
|
||||
BW.paused = on;
|
||||
BW.scene:SetPaused(on);
|
||||
for _, slot in ipairs(buildSlots) do
|
||||
slot.cd:SetPaused(on);
|
||||
end
|
||||
if on then
|
||||
BW.Drive(0, 0);
|
||||
end
|
||||
end
|
||||
|
||||
-- Shows the next waiting pick: the start's, then a chest's, then a level's. The game stays
|
||||
-- paused until the last pick is made.
|
||||
local function ShowCards()
|
||||
if BW.cards:IsShown() then
|
||||
return;
|
||||
end
|
||||
local title;
|
||||
if BW.run.startPick then
|
||||
BW.run.picking = "start";
|
||||
title = "Choose your first spell";
|
||||
elseif BW.run.chestPicks > 0 then
|
||||
BW.run.picking = "chest";
|
||||
title = BW.run.chestTitle;
|
||||
elseif BW.run.pendingLevels > 0 then
|
||||
BW.run.picking = "level";
|
||||
title = "Level " .. (BW.run.level - BW.run.pendingLevels + 1);
|
||||
else
|
||||
return;
|
||||
end
|
||||
BuildOffers();
|
||||
-- The three cards share the tallest one's height.
|
||||
local height = CARD_MIN_HEIGHT;
|
||||
for i = 1, 3 do
|
||||
height = math.max(height, FillCard(i, offers[i]));
|
||||
end
|
||||
for i = 1, 3 do
|
||||
getglobal(BW.cards:GetName() .. "Card" .. i):SetHeight(height);
|
||||
end
|
||||
getglobal(BW.cards:GetName() .. "Title"):SetText(title);
|
||||
BW.cards:Show();
|
||||
SetPaused(true);
|
||||
end
|
||||
|
||||
local function ApplyHeroSpeed()
|
||||
local factor = 1 + PASSIVES.sprint.per * (BW.run.passives.sprint or 0) / 100;
|
||||
if BW.run.swiftUntil then
|
||||
factor = factor * TUNING.SWIFTNESS_FACTOR;
|
||||
end
|
||||
if BW.run.shoutUntil then
|
||||
factor = factor * SPELLS.demoshout.mastery.speed;
|
||||
end
|
||||
BW.hero:SetMovementSpeed("run", BW.run.heroRun * factor);
|
||||
end
|
||||
|
||||
local function MaxHp()
|
||||
return HERO_HP + 25 * (BW.run.passives.vigor or 0);
|
||||
end
|
||||
|
||||
function Broodwatch_PickCard(i)
|
||||
if not BW.run or not BW.cards:IsShown() then
|
||||
return;
|
||||
end
|
||||
local o = offers[i];
|
||||
if o.kind == "fuse" then
|
||||
-- The fusion takes the first spell's place and recharge; the second
|
||||
-- spell's slot frees.
|
||||
local state = BW.run.spells[o.a];
|
||||
BW.run.spells[o.a] = nil;
|
||||
BW.run.spells[o.b] = nil;
|
||||
state.level = 1;
|
||||
BW.run.spells[o.key] = state;
|
||||
elseif o.kind == "evolve" then
|
||||
-- The evolution takes its spell's place, recharge and all.
|
||||
local state = BW.run.spells[o.from];
|
||||
BW.run.spells[o.from] = nil;
|
||||
state.level = 1;
|
||||
BW.run.spells[o.key] = state;
|
||||
elseif o.kind == "spell" then
|
||||
local state = BW.run.spells[o.key];
|
||||
if state then
|
||||
state.level = o.level;
|
||||
else
|
||||
BW.run.spells[o.key] = { level = 1, cooldown = 0, pending = 0 };
|
||||
end
|
||||
elseif o.kind == "gear" then
|
||||
BW.Equip(o.key);
|
||||
if GEAR[o.key].slot == "weapon" then
|
||||
BW.run.weaponPick = nil;
|
||||
end
|
||||
elseif o.kind == "passive" then
|
||||
BW.run.passives[o.key] = o.level;
|
||||
if o.key == "might" then
|
||||
BW.run.damageScale = 1 + 0.15 * o.level;
|
||||
elseif o.key == "vigor" then
|
||||
BW.run.hp = math.min(MaxHp(), BW.run.hp + 25);
|
||||
elseif o.key == "sprint" then
|
||||
ApplyHeroSpeed();
|
||||
end
|
||||
else
|
||||
BW.run.hp = math.min(MaxHp(), BW.run.hp + 50);
|
||||
end
|
||||
BW.cards:Hide();
|
||||
UpdateBuild();
|
||||
BW.UpdateHealth(); -- Vigor's larger bar and heal, Second Wind's heal (defined below)
|
||||
if BW.run.picking == "start" then
|
||||
BW.run.startPick = nil;
|
||||
elseif BW.run.picking == "chest" then
|
||||
BW.run.chestPicks = BW.run.chestPicks - 1;
|
||||
else
|
||||
BW.run.pendingLevels = BW.run.pendingLevels - 1;
|
||||
end
|
||||
ShowCards();
|
||||
if not BW.cards:IsShown() then
|
||||
SetPaused(false);
|
||||
-- A grace when the last card closes: the brood that crowded you while
|
||||
-- you leveled is thrown back, and nothing hurts you for a moment (the
|
||||
-- step; you show faded while it lasts).
|
||||
BW.run.graceUntil = BW.run.time + TUNING.GRACE_SECONDS;
|
||||
BW.hero:SetAlpha(TUNING.GRACE_ALPHA);
|
||||
local hx, hy = BW.hero:GetPosition();
|
||||
local r2 = TUNING.GRACE_RADIUS * TUNING.GRACE_RADIUS;
|
||||
for _, e in ipairs(BW.enemies) do
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
if BW.Dist2(x, y, hx, hy) <= r2 then
|
||||
BW.Blast(e, hx, hy, TUNING.GRACE_KNOCK);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function UpdateHealth(mode)
|
||||
local max = MaxHp();
|
||||
BW.healthBar:SetMinMaxValues(0, max, mode or EASE);
|
||||
BW.healthBar:SetValue(math.max(BW.run.hp, 0), mode or EASE);
|
||||
local text = string.format("%d / %d", math.max(math.ceil(BW.run.hp), 0), max);
|
||||
if BW.run.shield == math.huge then -- Divine Shield
|
||||
text = text .. " (immune)";
|
||||
elseif BW.run.shield then
|
||||
text = text .. string.format(" (+%d)", math.ceil(BW.run.shield));
|
||||
end
|
||||
if BW.run.invulnUntil then -- a Limited Invulnerability Potion
|
||||
text = text .. " (invulnerable)";
|
||||
end
|
||||
if BW.run.fireWard then -- a Fire Protection Potion
|
||||
text = text .. string.format(" (+%d fire)", math.ceil(BW.run.fireWard));
|
||||
end
|
||||
BW.healthText:SetText(text);
|
||||
end
|
||||
|
||||
local LEVEL_HEAL = 0.12 -- each level heals this share of the maximum health
|
||||
|
||||
-- The XP to the next level: cubic, so a strong run tops out around level 50
|
||||
-- and has to choose, rather than maxing everything (level 10 needs 55,
|
||||
-- level 30 needs 635).
|
||||
local function XpNeeded(level)
|
||||
return 5 + 3 * level + math.floor(level * level * level / 50);
|
||||
end
|
||||
|
||||
-- The experience bar and its text: the level, and the experience toward the
|
||||
-- next one.
|
||||
local function UpdateXp(mode)
|
||||
local needed = XpNeeded(BW.run.level);
|
||||
BW.xpBar:SetMinMaxValues(0, needed, mode);
|
||||
BW.xpBar:SetValue(BW.run.xp, mode);
|
||||
BW.xpText:SetText(string.format("Level %d %d / %d", BW.run.level, math.floor(BW.run.xp), needed));
|
||||
end
|
||||
|
||||
local function GainXp(xp)
|
||||
BW.run.xp = BW.run.xp + xp;
|
||||
local wrapped = false;
|
||||
while BW.run.xp >= XpNeeded(BW.run.level) do
|
||||
BW.run.xp = BW.run.xp - XpNeeded(BW.run.level);
|
||||
BW.run.level = BW.run.level + 1;
|
||||
BW.run.pendingLevels = BW.run.pendingLevels + 1;
|
||||
wrapped = true;
|
||||
end
|
||||
-- A level starts the bar again from empty at once; a gain within a
|
||||
-- level eases.
|
||||
if wrapped then
|
||||
BW.hero:PlayLevelUpEffect(); -- the world's glow and sound, once however many levels
|
||||
local levels = BW.run.pendingLevels;
|
||||
BW.run.hp = math.min(MaxHp(), BW.run.hp + MaxHp() * LEVEL_HEAL * levels);
|
||||
UpdateHealth();
|
||||
end
|
||||
UpdateXp(wrapped and AT_ONCE or EASE);
|
||||
ShowCards();
|
||||
end
|
||||
|
||||
-- For the files that load after this one.
|
||||
BW.Icon = Icon
|
||||
BW.UpdateBuild = UpdateBuild
|
||||
BW.SetPaused = SetPaused
|
||||
BW.ApplyHeroSpeed = ApplyHeroSpeed
|
||||
BW.MaxHp = MaxHp
|
||||
BW.UpdateHealth = UpdateHealth
|
||||
BW.XpNeeded = XpNeeded
|
||||
BW.GainXp = GainXp
|
||||
BW.UpdateXp = UpdateXp
|
||||
BW.ShowCards = ShowCards
|
||||
@@ -0,0 +1,407 @@
|
||||
-- Broodwatch items: drops and what each one does.
|
||||
|
||||
local _, BW = ...
|
||||
local Blast = BW.Blast
|
||||
local Hold = BW.Hold
|
||||
local ELITE = BW.ELITE
|
||||
local enemies = BW.enemies
|
||||
local gems = BW.gems
|
||||
local FloorAt = BW.FloorAt
|
||||
local Dist2 = BW.Dist2
|
||||
local Kill = BW.Kill
|
||||
local ShowNumber = BW.ShowNumber
|
||||
local Damage = BW.Damage
|
||||
local PackCenter = BW.PackCenter
|
||||
local markerPool = BW.markerPool
|
||||
local ReleaseMarker = BW.ReleaseMarker
|
||||
local UNKNOWN_ICON = BW.UNKNOWN_ICON
|
||||
local TUNING = BW.TUNING
|
||||
local ApplyHeroSpeed = BW.ApplyHeroSpeed
|
||||
local MaxHp = BW.MaxHp
|
||||
local UpdateHealth = BW.UpdateHealth
|
||||
local ITEMS = BW.ITEMS
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Items: a whelp drops one now and then, an elite always. A drop lies on the
|
||||
-- floor (a reticle under it, its icon bobbing over it) and is used when you
|
||||
-- walk over it. The icons are the items' use spells' (C_Spell.GetSpellTexture).
|
||||
|
||||
local ANIM_ATTACK_UNARMED = BW.ANIM_ATTACK_UNARMED
|
||||
|
||||
local drops = {}
|
||||
local dropPool = {}
|
||||
local chickens = {}
|
||||
local chickenPool = {}
|
||||
|
||||
local RunGrowth = BW.RunGrowth
|
||||
|
||||
local function PickItem()
|
||||
local total = 0;
|
||||
for _, item in ipairs(ITEMS) do
|
||||
total = total + item.weight;
|
||||
end
|
||||
local roll = math.random() * total;
|
||||
for _, item in ipairs(ITEMS) do
|
||||
if roll < item.weight then
|
||||
return item;
|
||||
end
|
||||
roll = roll - item.weight;
|
||||
end
|
||||
return ITEMS[1];
|
||||
end
|
||||
|
||||
BW.DropItem = function(e, x, y, z)
|
||||
if e.kind ~= ELITE and math.random() >= TUNING.ITEM_CHANCE then
|
||||
return;
|
||||
end
|
||||
local drop = table.remove(dropPool);
|
||||
if not drop then
|
||||
drop = { actor = BW.scene:AcquireActor(), icon = BW.feedback:CreateTexture(nil, "ARTWORK") };
|
||||
drop.icon:SetWidth(TUNING.ITEM_ICON);
|
||||
drop.icon:SetHeight(TUNING.ITEM_ICON);
|
||||
end
|
||||
drop.item = PickItem();
|
||||
drop.x, drop.y, drop.z = x, y, z;
|
||||
drop.life = TUNING.ITEM_SECONDS;
|
||||
drop.actor:SetPosition(x, y, z);
|
||||
drop.actor:SetGroundReticle(0.9);
|
||||
drop.actor:Show();
|
||||
drop.icon:SetTexture(drop.item.icon or UNKNOWN_ICON);
|
||||
drop.icon:Show();
|
||||
tinsert(drops, drop);
|
||||
end
|
||||
|
||||
local function ReleaseDrop(i)
|
||||
local drop = drops[i];
|
||||
drop.actor:ClearGroundReticle();
|
||||
drop.actor:Hide();
|
||||
drop.icon:Hide();
|
||||
tinsert(dropPool, drop);
|
||||
table.remove(drops, i);
|
||||
end
|
||||
|
||||
-- A thrown item (a grenade, dynamite) lands on the thickest pack: the
|
||||
-- reticle marks it while it flies.
|
||||
local function Throw(item)
|
||||
local x, y, z, target = PackCenter(24, item.radius);
|
||||
if not target then
|
||||
return;
|
||||
end
|
||||
local marker = table.remove(markerPool) or { actor = BW.scene:AcquireActor() };
|
||||
marker.actor:SetPosition(x, y, FloorAt(x, y, z));
|
||||
marker.actor:SetGroundReticle(item.radius);
|
||||
marker.actor:Show();
|
||||
local damage = item.damage * RunGrowth();
|
||||
local function Explode(entry)
|
||||
ReleaseMarker(marker);
|
||||
local ex, ey = entry.target.actor:GetPosition();
|
||||
local r2 = item.radius * item.radius;
|
||||
for _, o in ipairs(enemies) do
|
||||
if o.alive then
|
||||
local ox, oy = o.actor:GetPosition();
|
||||
if Dist2(ox, oy, ex, ey) <= r2 then
|
||||
if item.stun then
|
||||
Hold(o, item.stun, item.stunAura);
|
||||
end
|
||||
Damage(o, damage, TUNING.ITEM_COLOR);
|
||||
Blast(o, ex, ey, item.knock);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
local castID = BW.hero:PlaySpellVisual(item.spell, target.actor, 0);
|
||||
local entry = { target = target, onImpact = Explode };
|
||||
if castID then
|
||||
BW.run.inFlight[castID] = entry;
|
||||
else
|
||||
Explode(entry);
|
||||
end
|
||||
end
|
||||
|
||||
-- The Sapper Charge goes off on you: it clears the brood close by, and hurts.
|
||||
local function Sapper(item, hx, hy)
|
||||
BW.hero:PlaySpellVisual(item.spell, BW.hero, 0);
|
||||
local r2 = TUNING.SAPPER_RADIUS * TUNING.SAPPER_RADIUS;
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
if Dist2(x, y, hx, hy) <= r2 then
|
||||
if e.kind == ELITE then
|
||||
Damage(e, e.maxHp * TUNING.SAPPER_ELITE / BW.run.damageScale, TUNING.ITEM_COLOR);
|
||||
else
|
||||
Kill(e);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
BW.run.hp = math.max(1, BW.run.hp - MaxHp() * TUNING.SAPPER_SELF);
|
||||
UpdateHealth();
|
||||
end
|
||||
|
||||
local function SummonChicken(item, hx, hy, hz)
|
||||
BW.hero:PlaySpellVisual(item.spell, BW.hero, 0);
|
||||
local chicken = table.remove(chickenPool);
|
||||
if not chicken then
|
||||
chicken = { actor = BW.scene:AcquireActor() };
|
||||
chicken.actor:SetModelByCreatureDisplayID(TUNING.CHICKEN_DISPLAY);
|
||||
end
|
||||
chicken.life = TUNING.CHICKEN_SECONDS;
|
||||
chicken.peck = 0;
|
||||
chicken.move = 0;
|
||||
chicken.prey = nil;
|
||||
chicken.gone = nil;
|
||||
chicken.actor:SetPosition(hx + 1.5, hy, hz + 0.5);
|
||||
chicken.actor:Show();
|
||||
chicken.actor:SetMovementEnabled(true);
|
||||
chicken.actor:SetMovementSpeed("run", TUNING.CHICKEN_SPEED);
|
||||
tinsert(chickens, chicken);
|
||||
end
|
||||
|
||||
-- Goblin Bomb Dispenser: BOMB_COUNT walking bombs, each after the brood
|
||||
-- nearest to it; one goes off within BOMB_TRIGGER of its prey (the land
|
||||
-- mine's blast), or where it stands when its time runs out.
|
||||
local bombs = {}
|
||||
local bombPool = {}
|
||||
|
||||
local function DropBombs(hx, hy, hz)
|
||||
for k = 1, TUNING.BOMB_COUNT do
|
||||
local bomb = table.remove(bombPool);
|
||||
if not bomb then
|
||||
bomb = { actor = BW.scene:AcquireActor() };
|
||||
bomb.actor:SetModelByCreatureDisplayID(TUNING.BOMB_DISPLAY);
|
||||
end
|
||||
local a = k * 2 * math.pi / TUNING.BOMB_COUNT;
|
||||
bomb.life = TUNING.BOMB_LIFE;
|
||||
bomb.move = 0;
|
||||
bomb.actor:SetPosition(hx + 1.5 * math.cos(a), hy + 1.5 * math.sin(a), hz + 0.5);
|
||||
bomb.actor:Show();
|
||||
bomb.actor:SetMovementEnabled(true);
|
||||
bomb.actor:SetMovementSpeed("run", TUNING.BOMB_SPEED);
|
||||
tinsert(bombs, bomb);
|
||||
end
|
||||
end
|
||||
|
||||
local function NotAlive(e)
|
||||
return not e.alive;
|
||||
end
|
||||
|
||||
local function UpdateBombs(dt)
|
||||
local trigger2 = TUNING.BOMB_TRIGGER * TUNING.BOMB_TRIGGER;
|
||||
for i = table.getn(bombs), 1, -1 do
|
||||
local bomb = bombs[i];
|
||||
bomb.life = bomb.life - dt;
|
||||
bomb.move = bomb.move - dt;
|
||||
local x, y, z = bomb.actor:GetPosition();
|
||||
local prey = BW.Nearest(x, y, 40, NotAlive);
|
||||
local px, py, pz;
|
||||
if prey then
|
||||
px, py, pz = prey.actor:GetPosition();
|
||||
end
|
||||
if bomb.life <= 0 or (prey and Dist2(px, py, x, y) <= trigger2) then
|
||||
bomb.actor:PlaySpellVisualAtPoint(TUNING.BOMB_SPELL, x, y, z, 0);
|
||||
local r2 = TUNING.BOMB_RADIUS * TUNING.BOMB_RADIUS;
|
||||
local damage = TUNING.BOMB_DAMAGE * RunGrowth();
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive then
|
||||
local ex, ey = e.actor:GetPosition();
|
||||
if Dist2(ex, ey, x, y) <= r2 then
|
||||
Damage(e, damage, TUNING.ITEM_COLOR);
|
||||
Blast(e, x, y, TUNING.BOMB_KNOCK);
|
||||
end
|
||||
end
|
||||
end
|
||||
bomb.actor:SetMovementEnabled(false);
|
||||
bomb.actor:Hide();
|
||||
tinsert(bombPool, bomb);
|
||||
table.remove(bombs, i);
|
||||
elseif prey and bomb.move <= 0 then
|
||||
bomb.move = 0.25;
|
||||
bomb.actor:MoveTo(px, py, pz);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- The chicken's prey: the whelp nearest to it among those at the edge of
|
||||
-- the pack (past CHICKEN_HUNT_MIN from the hero, within the leash), else the
|
||||
-- nearest within the leash. It keeps its prey until the prey dies or strays.
|
||||
local function ChickenPrey(cx, cy, hx, hy)
|
||||
local leash2 = TUNING.CHICKEN_LEASH * TUNING.CHICKEN_LEASH;
|
||||
local min2 = TUNING.CHICKEN_HUNT_MIN * TUNING.CHICKEN_HUNT_MIN;
|
||||
local edge, edgeD2, any, anyD2;
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
local fromHero = Dist2(x, y, hx, hy);
|
||||
if fromHero <= leash2 then
|
||||
local d2 = Dist2(x, y, cx, cy);
|
||||
if fromHero >= min2 and (not edge or d2 < edgeD2) then
|
||||
edge, edgeD2 = e, d2;
|
||||
end
|
||||
if not any or d2 < anyD2 then
|
||||
any, anyD2 = e, d2;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return edge or any;
|
||||
end
|
||||
|
||||
local function UpdateChickens(dt, hx, hy, hz)
|
||||
local leash2 = TUNING.CHICKEN_LEASH * TUNING.CHICKEN_LEASH;
|
||||
local reach2 = TUNING.CHICKEN_REACH * TUNING.CHICKEN_REACH;
|
||||
local taunt2 = TUNING.CHICKEN_TAUNT_RADIUS * TUNING.CHICKEN_TAUNT_RADIUS;
|
||||
for i = table.getn(chickens), 1, -1 do
|
||||
local chicken = chickens[i];
|
||||
chicken.life = chicken.life - dt;
|
||||
if chicken.life <= 0 then
|
||||
chicken.gone = true;
|
||||
chicken.prey = nil;
|
||||
chicken.actor:SetMovementEnabled(false);
|
||||
chicken.actor:Hide();
|
||||
tinsert(chickenPool, chicken);
|
||||
table.remove(chickens, i);
|
||||
else
|
||||
local cx, cy, cz = chicken.actor:GetPosition();
|
||||
chicken.peck = chicken.peck - dt;
|
||||
chicken.move = chicken.move - dt;
|
||||
local prey = chicken.prey;
|
||||
if prey then
|
||||
local px, py = prey.actor:GetPosition();
|
||||
if not prey.alive or Dist2(px, py, hx, hy) > leash2 then
|
||||
prey = nil;
|
||||
end
|
||||
end
|
||||
if not prey then
|
||||
prey = ChickenPrey(cx, cy, hx, hy);
|
||||
chicken.prey = prey;
|
||||
end
|
||||
if prey then
|
||||
local tx, ty, tz = prey.actor:GetPosition();
|
||||
if Dist2(tx, ty, cx, cy) <= reach2 then
|
||||
if chicken.peck <= 0 then
|
||||
chicken.peck = TUNING.CHICKEN_PECK;
|
||||
chicken.actor:SetYaw(math.atan2(ty - cy, tx - cx));
|
||||
chicken.actor:SetAnimation(ANIM_ATTACK_UNARMED);
|
||||
Damage(prey, TUNING.CHICKEN_DAMAGE * RunGrowth(), TUNING.ITEM_COLOR);
|
||||
-- The peck taunts the whelps around the chicken: they
|
||||
-- chase it, and leave the hero be (Chase, Step).
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
if Dist2(x, y, cx, cy) <= taunt2 then
|
||||
e.taunter = chicken;
|
||||
e.tauntUntil = BW.run.time + TUNING.CHICKEN_TAUNT;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif chicken.move <= 0 then
|
||||
chicken.move = 0.25;
|
||||
chicken.actor:MoveTo(tx, ty, tz);
|
||||
end
|
||||
elseif chicken.move <= 0 then
|
||||
-- Nothing to hunt: it struts about the hero.
|
||||
chicken.move = 1.5 + math.random();
|
||||
local angle = math.random() * 2 * math.pi;
|
||||
local r = TUNING.CHICKEN_ROAM * (0.4 + 0.6 * math.random());
|
||||
chicken.actor:MoveTo(hx + r * math.cos(angle), hy + r * math.sin(angle), hz);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function UseItem(item, hx, hy, hz)
|
||||
BW.heroNumber.actor = BW.hero;
|
||||
ShowNumber(BW.heroNumber, 0, TUNING.ITEM_COLOR, item.name);
|
||||
if item.key == "potion" then
|
||||
BW.hero:PlaySpellVisual(item.spell, BW.hero, 0);
|
||||
local heal = math.floor(MaxHp() * TUNING.POTION_HEAL + 0.5);
|
||||
BW.run.hp = math.min(MaxHp(), BW.run.hp + heal);
|
||||
UpdateHealth();
|
||||
elseif item.key == "grenade" or item.key == "dynamite" then
|
||||
Throw(item);
|
||||
elseif item.key == "fireward" then
|
||||
BW.hero:PlaySpellVisual(item.spell, BW.hero, 0);
|
||||
BW.run.fireWard = MaxHp() * TUNING.FIREWARD_SHARE;
|
||||
BW.run.fireWardUntil = BW.run.time + TUNING.FIREWARD_SECONDS;
|
||||
UpdateHealth();
|
||||
elseif item.key == "invulnerable" then
|
||||
BW.hero:PlaySpellVisual(item.spell, BW.hero, 0);
|
||||
BW.run.invulnUntil = BW.run.time + TUNING.INVULNERABLE_SECONDS;
|
||||
UpdateHealth();
|
||||
elseif item.key == "bombs" then
|
||||
DropBombs(hx, hy, hz);
|
||||
elseif item.key == "swift" then
|
||||
BW.hero:PlaySpellVisual(item.spell, BW.hero, 0);
|
||||
BW.run.swiftUntil = BW.run.time + TUNING.SWIFTNESS_SECONDS;
|
||||
ApplyHeroSpeed();
|
||||
elseif item.key == "hearth" then
|
||||
BW.hero:PlaySpellVisual(item.spell, BW.hero, 0);
|
||||
for _, gem in ipairs(gems) do
|
||||
gem.pulled = true;
|
||||
end
|
||||
elseif item.key == "sapper" then
|
||||
Sapper(item, hx, hy);
|
||||
elseif item.key == "chicken" then
|
||||
SummonChicken(item, hx, hy, hz);
|
||||
end
|
||||
end
|
||||
|
||||
-- Whether a Battle Chicken holds this whelp: taunted, and the chicken out.
|
||||
local function Taunted(e)
|
||||
return e.taunter and not e.taunter.gone and BW.run.time < e.tauntUntil;
|
||||
end
|
||||
|
||||
local function UpdateItems(dt, hx, hy, hz)
|
||||
if BW.run.swiftUntil and BW.run.time >= BW.run.swiftUntil then
|
||||
BW.run.swiftUntil = nil;
|
||||
ApplyHeroSpeed();
|
||||
end
|
||||
local reach2 = TUNING.ITEM_REACH * TUNING.ITEM_REACH;
|
||||
local scale = BW.scene:GetEffectiveScale();
|
||||
local bob = math.sin(BW.run.time * 4) * 4;
|
||||
for i = table.getn(drops), 1, -1 do
|
||||
local drop = drops[i];
|
||||
drop.life = drop.life - dt;
|
||||
if Dist2(drop.x, drop.y, hx, hy) <= reach2 then
|
||||
local item = drop.item;
|
||||
ReleaseDrop(i);
|
||||
UseItem(item, hx, hy, hz);
|
||||
elseif drop.life <= 0 then
|
||||
ReleaseDrop(i);
|
||||
else
|
||||
local sx, sy = BW.scene:Project3DPointTo2D(drop.x, drop.y, drop.z + 1.2);
|
||||
if sx then
|
||||
drop.icon:ClearAllPoints();
|
||||
drop.icon:SetPoint("BOTTOM", BW.scene, "BOTTOMLEFT", sx / scale, sy / scale + bob);
|
||||
end
|
||||
end
|
||||
end
|
||||
UpdateChickens(dt, hx, hy, hz);
|
||||
UpdateBombs(dt);
|
||||
end
|
||||
|
||||
local function ClearItems()
|
||||
for i = table.getn(drops), 1, -1 do
|
||||
ReleaseDrop(i);
|
||||
end
|
||||
for i = table.getn(chickens), 1, -1 do
|
||||
chickens[i].gone = true;
|
||||
chickens[i].prey = nil;
|
||||
chickens[i].actor:SetMovementEnabled(false);
|
||||
chickens[i].actor:Hide();
|
||||
tinsert(chickenPool, chickens[i]);
|
||||
chickens[i] = nil;
|
||||
end
|
||||
for i = table.getn(bombs), 1, -1 do
|
||||
bombs[i].actor:SetMovementEnabled(false);
|
||||
bombs[i].actor:Hide();
|
||||
tinsert(bombPool, bombs[i]);
|
||||
bombs[i] = nil;
|
||||
end
|
||||
end
|
||||
|
||||
-- For the files that load after this one.
|
||||
BW.Taunted = Taunted
|
||||
BW.UpdateItems = UpdateItems
|
||||
BW.ClearItems = ClearItems
|
||||
@@ -0,0 +1,594 @@
|
||||
-- Broodwatch spell kinds beyond the first ones (Spells.lua): cones, turrets,
|
||||
-- zones, traps and charges. Each registers its Fire and Cast in
|
||||
-- BW.kindHandlers; the step runs UpdateKinds, a new run ClearKinds.
|
||||
|
||||
local _, BW = ...
|
||||
local SPELLS = BW.SPELLS
|
||||
local TUNING = BW.TUNING
|
||||
local enemies = BW.enemies
|
||||
local Dist2 = BW.Dist2
|
||||
local FloorAt = BW.FloorAt
|
||||
local Stat = BW.Stat
|
||||
local Mastered = BW.Mastered
|
||||
local SpellDamage = BW.SpellDamage
|
||||
local Burn = BW.Burn
|
||||
local Hold = BW.Hold
|
||||
local Blast = BW.Blast
|
||||
local Slow = BW.Slow
|
||||
local ShowAura = BW.ShowAura
|
||||
local Nearest = BW.Nearest
|
||||
local PackCenter = BW.PackCenter
|
||||
local QueueCast = BW.QueueCast
|
||||
|
||||
local handlers = BW.kindHandlers
|
||||
local FROZEN_AURA = 122 -- Frost Nova's ice, on what a cone freezes
|
||||
local IMPACTS = 8 -- enemies a hit shows its impact kit on, so a pack does not roar
|
||||
local ANIM_SPAWN, ANIM_CLOSED, ANIM_CUSTOM0 = 145, 147, 153
|
||||
|
||||
-- The usual Fire of a kind with no aim: one cast in the queue.
|
||||
local function QueueOne(key, state, def, level)
|
||||
QueueCast({ key = key, state = state, level = level });
|
||||
state.pending = state.pending + 1;
|
||||
return true;
|
||||
end
|
||||
|
||||
-- One cast of the spell's visual from `caster` at the first IMPACTS of
|
||||
-- `hit` (PlaySpellVisual with a table: the cast kit once, an impact kit on
|
||||
-- each), or at the caster itself when nothing was hit.
|
||||
local function ShowHits(caster, spell, hit)
|
||||
local actors = {};
|
||||
for i = 1, math.min(table.getn(hit), IMPACTS) do
|
||||
actors[i] = hit[i].actor;
|
||||
end
|
||||
if actors[1] then
|
||||
caster:PlaySpellVisual(spell, actors, 0);
|
||||
else
|
||||
caster:PlaySpellVisual(spell, caster, 0);
|
||||
end
|
||||
end
|
||||
|
||||
-- What a hit does besides its damage, by the spell's fields at `level`.
|
||||
local function Afflict(e, key, def, level, fromX, fromY)
|
||||
if not e.alive then
|
||||
return;
|
||||
end
|
||||
local slow = Stat(def, "slow", level);
|
||||
if slow then
|
||||
Slow(e, slow, 2);
|
||||
end
|
||||
local freeze = Stat(def, "freeze", level) or (Mastered(key) and def.mastery.freeze);
|
||||
if freeze then
|
||||
Hold(e, freeze, def.effect or FROZEN_AURA, true);
|
||||
end
|
||||
local ignite = Stat(def, "ignite", level);
|
||||
if ignite then
|
||||
Burn(e, ignite, def.igniteSeconds, def.burnSchool or def.school);
|
||||
end
|
||||
if def.knock then
|
||||
Blast(e, fromX, fromY, def.knock);
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Cones: everything within `length` of you, inside CONE_ARC (times `wide`)
|
||||
-- of the cone's aim. The aim is the direction that catches the most of the
|
||||
-- brood; the cast turns you to it for CAST_POSE (the cone hangs at your hand
|
||||
-- or breath), and you strafe or backpedal the way you hold meanwhile
|
||||
-- (Steer).
|
||||
|
||||
-- Counts the enemies in the cone aimed at `yaw`, or with `list` gathers them.
|
||||
local function InCone(def, level, hx, hy, yaw, list)
|
||||
local length = Stat(def, "length", level);
|
||||
local half = TUNING.CONE_ARC * (def.wide or 1) / 2;
|
||||
local l2 = length * length;
|
||||
local count = 0;
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
local dx, dy = x - hx, y - hy;
|
||||
local d2 = dx * dx + dy * dy;
|
||||
if d2 <= l2 then
|
||||
local off = math.mod(math.atan2(dy, dx) - yaw + 3 * math.pi, 2 * math.pi) - math.pi;
|
||||
if d2 < 1 or math.abs(off) <= half then
|
||||
count = count + 1;
|
||||
if list then
|
||||
tinsert(list, e);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return count;
|
||||
end
|
||||
|
||||
-- The aim that catches the most: tried toward each of up to AIM_TRIES
|
||||
-- enemies in reach. Nil when none is in reach.
|
||||
local AIM_TRIES = 16
|
||||
local function BestAim(def, level, hx, hy)
|
||||
local length = Stat(def, "length", level);
|
||||
local l2 = length * length;
|
||||
local best, bestCount, tried = nil, 0, 0;
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
if Dist2(x, y, hx, hy) <= l2 then
|
||||
local yaw = math.atan2(y - hy, x - hx);
|
||||
local count = InCone(def, level, hx, hy, yaw);
|
||||
if count > bestCount then
|
||||
best, bestCount = yaw, count;
|
||||
end
|
||||
tried = tried + 1;
|
||||
if tried >= AIM_TRIES then
|
||||
break;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return best;
|
||||
end
|
||||
|
||||
handlers.cone = {
|
||||
Fire = function(key, state, def, level)
|
||||
local hx, hy = BW.hero:GetPosition();
|
||||
return BestAim(def, level, hx, hy) ~= nil and QueueOne(key, state, def, level);
|
||||
end,
|
||||
Cast = function(entry, def)
|
||||
local hx, hy = BW.hero:GetPosition();
|
||||
local yaw = BestAim(def, entry.level, hx, hy);
|
||||
if not yaw then
|
||||
return;
|
||||
end
|
||||
-- The pose: you face the cone (Steer turns you, next frame).
|
||||
BW.run.pose = TUNING.CAST_POSE;
|
||||
BW.run.poseYaw = yaw;
|
||||
local hit = {};
|
||||
InCone(def, entry.level, hx, hy, yaw, hit);
|
||||
ShowHits(BW.hero, def.spell, hit);
|
||||
local damage = Stat(def, "damage", entry.level);
|
||||
for _, e in ipairs(hit) do
|
||||
SpellDamage(e, damage, def);
|
||||
Afflict(e, entry.key, def, entry.level, hx, hy);
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Totems: a turret shoots its `bolt` at the nearest enemies in range every
|
||||
-- `every` seconds (Fire Nova Totem also pulses as Magma Totem does); a zone
|
||||
-- slows every enemy near it, with its aura's visual on each.
|
||||
|
||||
local totems = {}
|
||||
local totemPool = {}
|
||||
local MAGMA_PULSE = 8187 -- Magma Totem's pulse: its visual puts an Immolate impact on each enemy
|
||||
|
||||
local function PlantTotem(key, def, level)
|
||||
local hx, hy, hz = BW.hero:GetPosition();
|
||||
local totem = table.remove(totemPool);
|
||||
if not totem then
|
||||
totem = { actor = BW.scene:AcquireActor() };
|
||||
totem.actor:HookScript("OnSpellVisualImpact", function(_, _, _, castID) BW.OnImpact(nil, nil, nil, castID); end);
|
||||
end
|
||||
if totem.display ~= def.display then
|
||||
totem.display = def.display;
|
||||
totem.actor:SetModelByCreatureDisplayID(def.display);
|
||||
end
|
||||
local a = math.random() * 2 * math.pi;
|
||||
totem.x, totem.y = hx + 2 * math.cos(a), hy + 2 * math.sin(a);
|
||||
totem.key, totem.level = key, level;
|
||||
totem.life = Stat(def, "duration", level);
|
||||
totem.shot, totem.pulse, totem.root = 0, def.pulseEvery or 0.5, 0;
|
||||
totem.actor:SetPosition(totem.x, totem.y, FloorAt(totem.x, totem.y, hz));
|
||||
totem.actor:Show();
|
||||
tinsert(totems, totem);
|
||||
end
|
||||
|
||||
local totemHit = {}
|
||||
local function NotTotemHit(e)
|
||||
return totemHit[e];
|
||||
end
|
||||
|
||||
local function Shoot(totem, def)
|
||||
local bolts = def.bolts or (Mastered(totem.key) and def.mastery.bolts) or 1;
|
||||
local damage = Stat(def, "damage", totem.level);
|
||||
for _ = 1, bolts do
|
||||
local target = Nearest(totem.x, totem.y, def.range, NotTotemHit);
|
||||
if not target then
|
||||
break;
|
||||
end
|
||||
totemHit[target] = true;
|
||||
local castID = totem.actor:PlaySpellVisual(def.bolt, target.actor, 0);
|
||||
if castID then
|
||||
BW.run.inFlight[castID] = { key = totem.key, target = target, damage = damage, level = totem.level };
|
||||
end
|
||||
end
|
||||
for e in pairs(totemHit) do
|
||||
totemHit[e] = nil;
|
||||
end
|
||||
end
|
||||
|
||||
-- Every enemy within the radius: `each(e)`; the first IMPACTS get `spell`'s
|
||||
-- impact from the totem.
|
||||
local function AroundTotem(totem, radius, spell, each)
|
||||
local r2 = radius * radius;
|
||||
local shown = 0;
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
if Dist2(x, y, totem.x, totem.y) <= r2 then
|
||||
if spell and shown < IMPACTS then
|
||||
shown = shown + 1;
|
||||
totem.actor:PlaySpellVisual(spell, e.actor, 0);
|
||||
end
|
||||
each(e);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function UpdateTotems(dt)
|
||||
for i = table.getn(totems), 1, -1 do
|
||||
local totem = totems[i];
|
||||
local def = SPELLS[totem.key];
|
||||
totem.life = totem.life - dt;
|
||||
if totem.life <= 0 then
|
||||
totem.actor:Hide();
|
||||
tinsert(totemPool, totem);
|
||||
table.remove(totems, i);
|
||||
elseif def.kind == "turret" then
|
||||
totem.shot = totem.shot - dt;
|
||||
if totem.shot <= 0 then
|
||||
totem.shot = totem.shot + def.every;
|
||||
Shoot(totem, def);
|
||||
end
|
||||
if def.pulse then
|
||||
totem.pulse = totem.pulse - dt;
|
||||
if totem.pulse <= 0 then
|
||||
totem.pulse = totem.pulse + def.pulseEvery;
|
||||
local damage = Stat(def, "pulseDamage", totem.level);
|
||||
AroundTotem(totem, Stat(def, "radius", totem.level), MAGMA_PULSE, function(e)
|
||||
SpellDamage(e, damage, def);
|
||||
end);
|
||||
end
|
||||
end
|
||||
else -- a zone
|
||||
totem.pulse = totem.pulse - dt;
|
||||
if totem.pulse <= 0 then
|
||||
totem.pulse = totem.pulse + 0.5;
|
||||
local slow = Stat(def, "slow", totem.level);
|
||||
local auraEnds = BW.run.time + 1;
|
||||
local rooting = false;
|
||||
if Mastered(totem.key) then
|
||||
totem.root = totem.root - 0.5;
|
||||
if totem.root <= 0 then
|
||||
totem.root = totem.root + def.mastery.every;
|
||||
rooting = true;
|
||||
end
|
||||
end
|
||||
AroundTotem(totem, Stat(def, "radius", totem.level), nil, function(e)
|
||||
Slow(e, slow, 1);
|
||||
ShowAura(e, def.aura, auraEnds);
|
||||
if rooting and e.alive then
|
||||
Hold(e, def.mastery.root);
|
||||
end
|
||||
end);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function ClearTotems()
|
||||
for i = table.getn(totems), 1, -1 do
|
||||
totems[i].actor:Hide();
|
||||
tinsert(totemPool, totems[i]);
|
||||
totems[i] = nil;
|
||||
end
|
||||
end
|
||||
|
||||
local totemKind = {
|
||||
Fire = QueueOne,
|
||||
Cast = function(entry, def)
|
||||
BW.hero:PlaySpellVisual(def.spell, BW.hero, 0);
|
||||
PlantTotem(entry.key, def, entry.level);
|
||||
end,
|
||||
}
|
||||
handlers.turret = totemKind
|
||||
handlers.zone = totemKind
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Traps: each lands at your feet (Spawn 145), arms after TRAP_ARM (Closed
|
||||
-- 147) and springs on the first whelp within TRAP_TRIGGER: its Custom0
|
||||
-- (153) and that slot's sound, then what the trap does to everything in
|
||||
-- its radius. A trap whose spell has an `area` (or Explosive Trap's
|
||||
-- mastery) leaves its ground burning or icy for a while.
|
||||
|
||||
local traps = {}
|
||||
local trapPool = {}
|
||||
local grounds = {}
|
||||
local groundPool = {}
|
||||
|
||||
local function DropTrap(i)
|
||||
local trap = traps[i];
|
||||
trap.actor:ClearGroundReticle();
|
||||
trap.actor:Hide();
|
||||
tinsert(trapPool, trap);
|
||||
table.remove(traps, i);
|
||||
end
|
||||
|
||||
local function LayTrap(key, def, level, x, y, z)
|
||||
if table.getn(traps) >= TUNING.TRAP_MAX then
|
||||
DropTrap(1);
|
||||
end
|
||||
local trap = table.remove(trapPool) or { actor = BW.scene:AcquireActor() };
|
||||
if trap.path ~= def.path then
|
||||
trap.path = def.path;
|
||||
trap.actor:SetModelByPath(def.path);
|
||||
trap.actor:SetScale(TUNING.TRAP_SCALE);
|
||||
end
|
||||
trap.key, trap.level = key, level;
|
||||
trap.x, trap.y, trap.z = x, y, FloorAt(x, y, z);
|
||||
trap.state = "arming";
|
||||
trap.left = TUNING.TRAP_ARM;
|
||||
trap.life = TUNING.TRAP_LIFE;
|
||||
trap.springs = Mastered(key) and def.mastery.springs or 1;
|
||||
trap.actor:SetPosition(trap.x, trap.y, trap.z);
|
||||
trap.actor:SetYaw(math.random() * 2 * math.pi);
|
||||
trap.actor:SetAnimation(ANIM_SPAWN);
|
||||
trap.actor:SetGroundReticle(0); -- small while it arms
|
||||
trap.actor:Show();
|
||||
tinsert(traps, trap);
|
||||
end
|
||||
|
||||
local function LayGround(spell, x, y, z, radius, seconds, def, level)
|
||||
local ground = table.remove(groundPool) or { actor = BW.scene:AcquireActor() };
|
||||
ground.def, ground.level = def, level;
|
||||
ground.x, ground.y, ground.radius = x, y, radius;
|
||||
ground.life, ground.tick = seconds, 0;
|
||||
ground.actor:SetModelBySpellArea(spell, radius);
|
||||
ground.actor:SetPosition(x, y, z);
|
||||
ground.actor:Show();
|
||||
tinsert(grounds, ground);
|
||||
end
|
||||
|
||||
local function Spring(trap)
|
||||
local def = SPELLS[trap.key];
|
||||
trap.springs = trap.springs - 1;
|
||||
trap.state = trap.springs > 0 and "arming" or "sprung";
|
||||
trap.left = trap.springs > 0 and TUNING.TRAP_ARM or TUNING.TRAP_SPRUNG;
|
||||
trap.actor:SetAnimation(ANIM_CUSTOM0);
|
||||
trap.actor:PlaySound(def.sound);
|
||||
trap.actor:SetGroundReticle(0);
|
||||
local radius = Stat(def, "radius", trap.level);
|
||||
local r2 = radius * radius;
|
||||
local hit = {};
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
if Dist2(x, y, trap.x, trap.y) <= r2 then
|
||||
tinsert(hit, e);
|
||||
end
|
||||
end
|
||||
end
|
||||
-- The effect's impact on what it hits (Explosive Trap Effect's molten
|
||||
-- blast); the freeze shows its own aura.
|
||||
if def.effect ~= 3355 then
|
||||
ShowHits(trap.actor, def.effect, hit);
|
||||
end
|
||||
local damage = Stat(def, "damage", trap.level);
|
||||
for _, e in ipairs(hit) do
|
||||
if damage then
|
||||
SpellDamage(e, damage, def);
|
||||
end
|
||||
Afflict(e, trap.key, def, trap.level, trap.x, trap.y);
|
||||
end
|
||||
if def.area then
|
||||
LayGround(def.area, trap.x, trap.y, trap.z, radius, Stat(def, "duration", trap.level), def, trap.level);
|
||||
elseif Mastered(trap.key) and def.mastery.area then
|
||||
LayGround(def.effect, trap.x, trap.y, trap.z, radius, def.mastery.area, def, trap.level);
|
||||
end
|
||||
end
|
||||
|
||||
-- A whelp (an enemy that moves) on the trap.
|
||||
local function Stepped(trap)
|
||||
local t2 = TUNING.TRAP_TRIGGER * TUNING.TRAP_TRIGGER;
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive and e.kind.speed > 0 then
|
||||
local x, y = e.actor:GetPosition();
|
||||
if Dist2(x, y, trap.x, trap.y) <= t2 then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
end
|
||||
return false;
|
||||
end
|
||||
|
||||
local function UpdateTraps(dt)
|
||||
for i = table.getn(traps), 1, -1 do
|
||||
local trap = traps[i];
|
||||
trap.life = trap.life - dt;
|
||||
if trap.state == "armed" then
|
||||
if Stepped(trap) then
|
||||
Spring(trap);
|
||||
elseif trap.life <= 0 then
|
||||
DropTrap(i);
|
||||
end
|
||||
else
|
||||
trap.left = trap.left - dt;
|
||||
if trap.left <= 0 then
|
||||
if trap.state == "arming" then
|
||||
trap.state = "armed";
|
||||
trap.actor:SetAnimation(ANIM_CLOSED);
|
||||
-- Armed: the ring shows what it will catch.
|
||||
trap.actor:SetGroundReticle(Stat(SPELLS[trap.key], "radius", trap.level));
|
||||
else
|
||||
DropTrap(i);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-- The grounds: each second they burn (a burning ground) or chill (an
|
||||
-- icy one) everything on them.
|
||||
for i = table.getn(grounds), 1, -1 do
|
||||
local ground = grounds[i];
|
||||
ground.life = ground.life - dt;
|
||||
ground.tick = ground.tick - dt;
|
||||
if ground.tick <= 0 then
|
||||
ground.tick = ground.tick + 1;
|
||||
local def = ground.def;
|
||||
local ignite = Stat(def, "ignite", ground.level);
|
||||
local slow = Stat(def, "slow", ground.level);
|
||||
local r2 = ground.radius * ground.radius;
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
if Dist2(x, y, ground.x, ground.y) <= r2 then
|
||||
if ignite then
|
||||
Burn(e, ignite, def.igniteSeconds, def.school);
|
||||
end
|
||||
if slow then
|
||||
Slow(e, slow, 1.2);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if ground.life <= 0 then
|
||||
ground.actor:Hide();
|
||||
tinsert(groundPool, ground);
|
||||
table.remove(grounds, i);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function ClearTraps()
|
||||
for i = table.getn(traps), 1, -1 do
|
||||
DropTrap(i);
|
||||
end
|
||||
for i = table.getn(grounds), 1, -1 do
|
||||
grounds[i].actor:Hide();
|
||||
tinsert(groundPool, grounds[i]);
|
||||
grounds[i] = nil;
|
||||
end
|
||||
end
|
||||
|
||||
handlers.trap = {
|
||||
Fire = QueueOne,
|
||||
Cast = function(entry, def)
|
||||
BW.hero:PlaySpellVisual(def.spell, BW.hero, 0);
|
||||
local hx, hy, hz = BW.hero:GetPosition();
|
||||
local n = Stat(def, "traps", entry.level);
|
||||
for k = 1, n do
|
||||
-- The first at your feet, the others in a ring around it.
|
||||
local r = k == 1 and 0 or 2.5;
|
||||
local a = (k - 2) * 2 * math.pi / math.max(n - 1, 1) + BW.run.heroYaw;
|
||||
LayTrap(entry.key, def, entry.level, hx + r * math.cos(a), hy + r * math.sin(a), hz);
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Charges: you rush at CHARGE_SPEED into the thickest pack in range, past
|
||||
-- your control until you reach it or CHARGE_MAX runs out; where you land,
|
||||
-- the brood around you is hurt and stunned (Intercept spins first).
|
||||
|
||||
local CHARGE_MIN = 5 -- yards: a pack closer than this is not worth the rush
|
||||
|
||||
handlers.charge = {
|
||||
Fire = function(key, state, def, level)
|
||||
if BW.run.charge then
|
||||
return false;
|
||||
end
|
||||
local x, y, _, target = PackCenter(Stat(def, "range", level), Stat(def, "radius", level));
|
||||
local hx, hy = BW.hero:GetPosition();
|
||||
if not target or Dist2(x, y, hx, hy) < CHARGE_MIN * CHARGE_MIN then
|
||||
return false;
|
||||
end
|
||||
QueueCast({ key = key, state = state, level = level, goal = target });
|
||||
state.pending = state.pending + 1;
|
||||
return true;
|
||||
end,
|
||||
Cast = function(entry, def)
|
||||
if not entry.goal.alive then
|
||||
return;
|
||||
end
|
||||
BW.hero:PlaySpellVisual(def.spell, entry.goal.actor, 0);
|
||||
BW.run.charge = { key = entry.key, level = entry.level, goal = entry.goal, left = TUNING.CHARGE_MAX };
|
||||
BW.hero:SetMovementSpeed("run", TUNING.CHARGE_SPEED);
|
||||
local hx, hy = BW.hero:GetPosition();
|
||||
local gx, gy = entry.goal.actor:GetPosition();
|
||||
BW.Drive(1, 0, math.atan2(gy - hy, gx - hx));
|
||||
end,
|
||||
}
|
||||
|
||||
local function Land(charge)
|
||||
local def = SPELLS[charge.key];
|
||||
BW.run.charge = nil;
|
||||
BW.Drive(0, 0);
|
||||
BW.ApplyHeroSpeed();
|
||||
if def.whirl then
|
||||
BW.hero:PlaySpellVisual(def.whirl, BW.hero, 0);
|
||||
end
|
||||
local hx, hy = BW.hero:GetPosition();
|
||||
local radius = Stat(def, "radius", charge.level);
|
||||
local r2 = radius * radius;
|
||||
local damage = Stat(def, "damage", charge.level);
|
||||
local stun = Stat(def, "stun", charge.level);
|
||||
local knock = Mastered(charge.key) and def.mastery.knock;
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
if Dist2(x, y, hx, hy) <= r2 then
|
||||
SpellDamage(e, damage, def);
|
||||
if e.alive then
|
||||
Hold(e, stun, def.stunAura);
|
||||
end
|
||||
if knock then
|
||||
Blast(e, hx, hy, knock);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function UpdateCharge(dt)
|
||||
local charge = BW.run.charge;
|
||||
if not charge then
|
||||
return;
|
||||
end
|
||||
charge.left = charge.left - dt;
|
||||
local hx, hy = BW.hero:GetPosition();
|
||||
local goal = charge.goal;
|
||||
local gx, gy = goal.actor:GetPosition();
|
||||
if charge.left <= 0 or not goal.alive or Dist2(gx, gy, hx, hy) <= 2.5 * 2.5 then
|
||||
Land(charge);
|
||||
return;
|
||||
end
|
||||
-- Steer at the goal as it moves.
|
||||
BW.Drive(1, 0, math.atan2(gy - hy, gx - hx));
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local function UpdateKinds(dt)
|
||||
if BW.run.pose then
|
||||
BW.run.pose = BW.run.pose - dt;
|
||||
if BW.run.pose <= 0 then
|
||||
BW.run.pose, BW.run.poseYaw = nil, nil; -- Steer turns you back to the way you hold
|
||||
end
|
||||
end
|
||||
UpdateTotems(dt);
|
||||
UpdateTraps(dt);
|
||||
UpdateCharge(dt);
|
||||
end
|
||||
|
||||
local function ClearKinds()
|
||||
ClearTotems();
|
||||
ClearTraps();
|
||||
if BW.run and BW.run.charge then
|
||||
BW.run.charge = nil;
|
||||
BW.ApplyHeroSpeed();
|
||||
end
|
||||
end
|
||||
|
||||
-- For the files that load after this one.
|
||||
BW.UpdateKinds = UpdateKinds
|
||||
BW.ClearKinds = ClearKinds
|
||||
@@ -0,0 +1,110 @@
|
||||
-- Broodwatch markers: an arrow at the scene's edge for each closed chest and
|
||||
-- each clutch of eggs out of sight.
|
||||
|
||||
local _, BW = ...
|
||||
local TUNING = BW.TUNING
|
||||
local EGG = BW.EGG
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Each frame a target is projected onto the scene (Project3DPointTo2D). One
|
||||
-- that lands inside the scene, less MARKER_MARGIN, needs no marker. One
|
||||
-- outside gets an arrow on the line from the scene's centre to it, where
|
||||
-- that line meets the margin, turned to point at it, with its icon just
|
||||
-- inside the arrow. A clutch shows once, at where it was laid, with the
|
||||
-- seconds left until it hatches.
|
||||
|
||||
local markers = {}
|
||||
local frameNo = 0 -- marks a clutch already shown this frame
|
||||
|
||||
local function TakeMarker(i)
|
||||
local marker = markers[i];
|
||||
if not marker then
|
||||
marker = {
|
||||
arrow = BW.feedback:CreateTexture(nil, "OVERLAY"),
|
||||
icon = BW.feedback:CreateTexture(nil, "ARTWORK"),
|
||||
text = BW.feedback:CreateFontString(nil, "OVERLAY"),
|
||||
};
|
||||
marker.text:SetFontObject(NumberFontNormal);
|
||||
marker.arrow:SetTexture(TUNING.MARKER_ARROW);
|
||||
marker.arrow:SetWidth(TUNING.MARKER_ARROW_SIZE);
|
||||
marker.arrow:SetHeight(TUNING.MARKER_ARROW_SIZE);
|
||||
marker.icon:SetWidth(TUNING.MARKER_ICON_SIZE);
|
||||
marker.icon:SetHeight(TUNING.MARKER_ICON_SIZE);
|
||||
marker.text:SetPoint("CENTER", marker.icon, "CENTER", 0, 0);
|
||||
markers[i] = marker;
|
||||
end
|
||||
return marker;
|
||||
end
|
||||
|
||||
local function HideMarker(marker)
|
||||
marker.arrow:Hide();
|
||||
marker.icon:Hide();
|
||||
marker.text:Hide();
|
||||
end
|
||||
|
||||
-- Shows marker `used + 1` for the point when it is off the scene; returns
|
||||
-- the count of markers used.
|
||||
local function Point(used, x, y, z, icon, text)
|
||||
local scene = BW.scene;
|
||||
local sx, sy = scene:Project3DPointTo2D(x, y, z);
|
||||
if not sx then
|
||||
return used;
|
||||
end
|
||||
local scale = scene:GetEffectiveScale();
|
||||
local w, h = scene:GetWidth(), scene:GetHeight();
|
||||
local m = TUNING.MARKER_MARGIN;
|
||||
sx, sy = sx / scale, sy / scale;
|
||||
if sx >= m and sx <= w - m and sy >= m and sy <= h - m then
|
||||
return used;
|
||||
end
|
||||
local cx, cy = w / 2, h / 2;
|
||||
local dx, dy = sx - cx, sy - cy;
|
||||
-- Where the line from the centre leaves the box inset by the margin.
|
||||
local t = math.min(dx ~= 0 and (cx - m) / math.abs(dx) or math.huge,
|
||||
dy ~= 0 and (cy - m) / math.abs(dy) or math.huge);
|
||||
local ex, ey = cx + dx * t, cy + dy * t;
|
||||
local d = math.sqrt(dx * dx + dy * dy);
|
||||
local ux, uy = dx / d, dy / d;
|
||||
used = used + 1;
|
||||
local marker = TakeMarker(used);
|
||||
marker.arrow:ClearAllPoints();
|
||||
marker.arrow:SetPoint("CENTER", scene, "BOTTOMLEFT", ex, ey);
|
||||
marker.arrow:SetRotation(math.atan2(dy, dx));
|
||||
marker.arrow:Show();
|
||||
local inset = (TUNING.MARKER_ARROW_SIZE + TUNING.MARKER_ICON_SIZE) / 2;
|
||||
marker.icon:ClearAllPoints();
|
||||
marker.icon:SetPoint("CENTER", scene, "BOTTOMLEFT", ex - ux * inset, ey - uy * inset);
|
||||
marker.icon:SetTexture(icon);
|
||||
marker.icon:Show();
|
||||
if text then
|
||||
marker.text:SetText(text);
|
||||
marker.text:Show();
|
||||
else
|
||||
marker.text:Hide();
|
||||
end
|
||||
return used;
|
||||
end
|
||||
|
||||
local function UpdateMarkers()
|
||||
frameNo = frameNo + 1;
|
||||
local used = 0;
|
||||
for _, chest in ipairs(BW.chests) do
|
||||
if chest.state == "closed" then
|
||||
used = Point(used, chest.x, chest.y, chest.z + 1, chest.kind.icon);
|
||||
end
|
||||
end
|
||||
for _, e in ipairs(BW.eggs) do
|
||||
local clutch = e.clutch;
|
||||
if e.alive and clutch.shown ~= frameNo then
|
||||
clutch.shown = frameNo;
|
||||
local left = math.max(0, math.ceil(clutch.hatchAt - BW.run.time));
|
||||
used = Point(used, clutch.x, clutch.y, clutch.z + 1, EGG.icon, tostring(left));
|
||||
end
|
||||
end
|
||||
for i = used + 1, table.getn(markers) do
|
||||
HideMarker(markers[i]);
|
||||
end
|
||||
end
|
||||
|
||||
-- For the files that load after this one.
|
||||
BW.UpdateMarkers = UpdateMarkers
|
||||
@@ -0,0 +1,131 @@
|
||||
-- Broodwatch mines: Goblin Land Mines in the lair. The brood sets them off;
|
||||
-- you lead it over them.
|
||||
|
||||
local _, BW = ...
|
||||
local TUNING = BW.TUNING
|
||||
local enemies = BW.enemies
|
||||
local Dist2 = BW.Dist2
|
||||
local FloorAt = BW.FloorAt
|
||||
local RunGrowth = BW.RunGrowth
|
||||
local SpawnPoint = BW.SpawnPoint
|
||||
local Damage = BW.Damage
|
||||
local Blast = BW.Blast
|
||||
|
||||
local ANIM_STAND = 0
|
||||
local ANIM_DESPAWN = 157
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- A mine is not an enemy: no spell aims at it, and the hero walks over it
|
||||
-- safely. It lies dormant under a small reticle until the hero steps within
|
||||
-- MINE_TRIGGER: that arms it (Land Mine Arming's sound), and its reticle
|
||||
-- grows to its blast. An armed mine waits for a whelp (any enemy that moves)
|
||||
-- within MINE_TRIGGER. Then it blows: every enemy within MINE_RADIUS is hurt
|
||||
-- and thrown, the mine plays Despawn and hides, and MINE_RESPAWN seconds
|
||||
-- later it lies dormant somewhere else. So a mine works where you lead the
|
||||
-- brood, not wherever it wanders.
|
||||
|
||||
local mines = {}
|
||||
|
||||
local function Place(mine)
|
||||
local x, y = SpawnPoint();
|
||||
local _, _, hz = BW.hero:GetPosition();
|
||||
mine.x, mine.y, mine.z = x, y, FloorAt(x, y, hz);
|
||||
mine.state = "dormant";
|
||||
mine.actor:SetPosition(mine.x, mine.y, mine.z);
|
||||
mine.actor:SetYaw(math.random() * 2 * math.pi);
|
||||
mine.actor:SetAnimation(ANIM_STAND);
|
||||
mine.actor:SetGroundReticle(0);
|
||||
mine.actor:Show();
|
||||
end
|
||||
|
||||
local function Arm(mine)
|
||||
mine.state = "armed";
|
||||
mine.actor:SetGroundReticle(TUNING.MINE_RADIUS);
|
||||
mine.actor:PlaySound(TUNING.MINE_ARM_SOUND);
|
||||
end
|
||||
|
||||
-- Lays MINE_COUNT armed mines (a new run).
|
||||
local function LayMines()
|
||||
for i = 1, TUNING.MINE_COUNT do
|
||||
local mine = mines[i];
|
||||
if not mine then
|
||||
mine = { actor = BW.scene:AcquireActor() };
|
||||
mine.actor:SetModelByCreatureDisplayID(TUNING.MINE_DISPLAY);
|
||||
mines[i] = mine;
|
||||
end
|
||||
Place(mine);
|
||||
end
|
||||
end
|
||||
|
||||
local function Blow(mine)
|
||||
mine.state = "blowing";
|
||||
mine.left = TUNING.MINE_DESPAWN;
|
||||
mine.actor:ClearGroundReticle();
|
||||
mine.actor:PlaySpellVisualAtPoint(TUNING.MINE_SPELL, mine.x, mine.y, mine.z, 0);
|
||||
mine.actor:SetAnimation(ANIM_DESPAWN);
|
||||
local r2 = TUNING.MINE_RADIUS * TUNING.MINE_RADIUS;
|
||||
local damage = TUNING.MINE_DAMAGE * RunGrowth();
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
if Dist2(x, y, mine.x, mine.y) <= r2 then
|
||||
Damage(e, damage, TUNING.MINE_COLOR);
|
||||
Blast(e, mine.x, mine.y, TUNING.MINE_KNOCK);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Whether a whelp stands on the mine.
|
||||
local function Stepped(mine)
|
||||
local t2 = TUNING.MINE_TRIGGER * TUNING.MINE_TRIGGER;
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive and e.kind.speed > 0 then
|
||||
local x, y = e.actor:GetPosition();
|
||||
if Dist2(x, y, mine.x, mine.y) <= t2 then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
end
|
||||
return false;
|
||||
end
|
||||
|
||||
local function UpdateMines(dt, hx, hy)
|
||||
local t2 = TUNING.MINE_TRIGGER * TUNING.MINE_TRIGGER;
|
||||
for _, mine in ipairs(mines) do
|
||||
if mine.state == "dormant" then
|
||||
if Dist2(hx, hy, mine.x, mine.y) <= t2 then
|
||||
Arm(mine);
|
||||
end
|
||||
elseif mine.state == "armed" then
|
||||
if Stepped(mine) then
|
||||
Blow(mine);
|
||||
end
|
||||
else
|
||||
mine.left = mine.left - dt;
|
||||
if mine.left <= 0 then
|
||||
if mine.state == "blowing" then
|
||||
mine.state = "spent";
|
||||
mine.left = TUNING.MINE_RESPAWN;
|
||||
mine.actor:Hide();
|
||||
else
|
||||
Place(mine);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function ClearMines()
|
||||
for _, mine in ipairs(mines) do
|
||||
mine.state = "spent";
|
||||
mine.left = 0;
|
||||
mine.actor:ClearGroundReticle();
|
||||
mine.actor:Hide();
|
||||
end
|
||||
end
|
||||
|
||||
-- For the files that load after this one.
|
||||
BW.LayMines = LayMines
|
||||
BW.UpdateMines = UpdateMines
|
||||
BW.ClearMines = ClearMines
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,123 @@
|
||||
-- Broodwatch vents: the lair's lava cracks erupt in waves, and burn
|
||||
-- whoever stands on them, you or the brood.
|
||||
|
||||
local _, BW = ...
|
||||
local VENTS = BW.VENTS
|
||||
local VENT_PATH = BW.VENT_PATH
|
||||
local VENT_MIRROR_PATH = BW.VENT_MIRROR_PATH
|
||||
local TUNING = BW.TUNING
|
||||
local enemies = BW.enemies
|
||||
local Dist2 = BW.Dist2
|
||||
local RunGrowth = BW.RunGrowth
|
||||
local Damage = BW.Damage
|
||||
local Blast = BW.Blast
|
||||
local MaxHp = BW.MaxHp
|
||||
|
||||
local ANIM_STAND = 0
|
||||
local ANIM_ERUPT = 153 -- the crack's Custom0
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Each crack is an actor at its gameobject's spawn, built once. A crack is
|
||||
-- idle, marked (a red ring, VENT_WARN seconds) or erupting (VENT_ERUPT
|
||||
-- seconds). Only idle cracks are picked for a wave. The cracks of a wave
|
||||
-- erupt together, and one of them plays the sound: a sound per crack at
|
||||
-- once roars.
|
||||
|
||||
local vents = {}
|
||||
|
||||
local function Build()
|
||||
for _, row in ipairs(VENTS) do
|
||||
local vent = { actor = BW.scene:AcquireActor() };
|
||||
vent.x, vent.y, vent.z = row[1], row[2], row[3];
|
||||
vent.radius = TUNING.VENT_RADIUS * row[5];
|
||||
vent.actor:SetModelByPath(row[6] and VENT_MIRROR_PATH or VENT_PATH);
|
||||
vent.actor:SetPosition(vent.x, vent.y, vent.z);
|
||||
vent.actor:SetYaw(row[4]);
|
||||
vent.actor:SetScale(row[5]);
|
||||
vent.actor:Show();
|
||||
tinsert(vents, vent);
|
||||
end
|
||||
end
|
||||
|
||||
local function Idle(vent)
|
||||
vent.state = "idle";
|
||||
vent.actor:ClearGroundReticle();
|
||||
vent.actor:SetAnimation(ANIM_STAND);
|
||||
end
|
||||
|
||||
-- A new run: every crack idle, the first wave at VENT_FIRST.
|
||||
local function ResetVents()
|
||||
if not vents[1] then
|
||||
Build();
|
||||
end
|
||||
for _, vent in ipairs(vents) do
|
||||
Idle(vent);
|
||||
end
|
||||
BW.run.nextVents = TUNING.VENT_FIRST;
|
||||
end
|
||||
|
||||
local function MarkWave()
|
||||
local size = 1 + math.floor((BW.run.time - TUNING.VENT_FIRST) / TUNING.VENT_WAVE_GROWTH);
|
||||
size = math.max(1, math.min(size, TUNING.VENT_WAVE_MAX)); -- at least one (a test's early wave)
|
||||
for _ = 1, size do
|
||||
-- A random idle crack (a few tries; a busy lair skips the rest).
|
||||
for _ = 1, 5 do
|
||||
local vent = vents[math.random(table.getn(vents))];
|
||||
if vent.state == "idle" then
|
||||
vent.state = "marked";
|
||||
vent.left = TUNING.VENT_WARN;
|
||||
vent.actor:SetGroundReticle(vent.radius, false);
|
||||
break;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function Erupt(vent, hx, hy, sound)
|
||||
vent.state = "erupting";
|
||||
vent.left = TUNING.VENT_ERUPT;
|
||||
vent.actor:ClearGroundReticle();
|
||||
vent.actor:SetAnimation(ANIM_ERUPT);
|
||||
if sound then
|
||||
vent.actor:PlaySound(TUNING.VENT_SOUND);
|
||||
end
|
||||
local r2 = vent.radius * vent.radius;
|
||||
if Dist2(hx, hy, vent.x, vent.y) <= r2 then
|
||||
BW.HurtHero(MaxHp() * TUNING.VENT_HERO, "Fire");
|
||||
end
|
||||
local damage = TUNING.VENT_DAMAGE * RunGrowth();
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
if Dist2(x, y, vent.x, vent.y) <= r2 then
|
||||
Damage(e, damage, TUNING.VENT_COLOR);
|
||||
Blast(e, vent.x, vent.y, TUNING.VENT_KNOCK);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function UpdateVents(dt, hx, hy)
|
||||
if BW.run.time >= BW.run.nextVents then
|
||||
BW.run.nextVents = BW.run.nextVents + TUNING.VENT_EVERY;
|
||||
MarkWave();
|
||||
end
|
||||
local sounded = false;
|
||||
for _, vent in ipairs(vents) do
|
||||
if vent.state ~= "idle" then
|
||||
vent.left = vent.left - dt;
|
||||
if vent.left <= 0 then
|
||||
if vent.state == "marked" then
|
||||
Erupt(vent, hx, hy, not sounded);
|
||||
sounded = true;
|
||||
else
|
||||
Idle(vent);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- For the files that load after this one.
|
||||
BW.ResetVents = ResetVents
|
||||
BW.UpdateVents = UpdateVents
|
||||
+10
-8
@@ -76,20 +76,22 @@ endif()
|
||||
add_library(${PROJECT_NAME} SHARED
|
||||
${SOURCES} ${HEADERS} ${TINYCBOR_SOURCES})
|
||||
|
||||
# Embed the addons under `src/embed/AddOns/` and `src/bindings/Bindings.xml` as RCDATA
|
||||
# resources. The file-read hook in `src/addons/Embedded.cpp` serves the
|
||||
# addon files when the engine asks for paths under
|
||||
# `Interface\AddOns\<addon>\`, so the addons load even when the user
|
||||
# doesn't have them on disk. `src/bindings/Inject.cpp` splices the bindings
|
||||
# XML (our TARGETING-group additions) into the engine's
|
||||
# `Interface\FrameXML\Bindings.xml` at load time.
|
||||
# Embed the addons under `src/embed/AddOns/`, `src/bindings/Bindings.xml` and
|
||||
# the textures under `src/embed/Textures/` as RCDATA resources. The file-read
|
||||
# hook in `src/addons/Embedded.cpp` serves the addon files when the engine
|
||||
# asks for paths under `Interface\AddOns\<addon>\`, so the addons load even
|
||||
# when the user doesn't have them on disk. `src/bindings/Inject.cpp` splices
|
||||
# the bindings XML (our TARGETING-group additions) into the engine's
|
||||
# `Interface\FrameXML\Bindings.xml` at load time. `src/texture/Builtin.cpp`
|
||||
# serves each texture at its game path when no file has it.
|
||||
include(cmake/embed_resources.cmake)
|
||||
embed_resources(
|
||||
TARGET ${PROJECT_NAME}
|
||||
ADDONS_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/src/embed/AddOns"
|
||||
ADDONS "!!!ClassicAPI" "ClassicAPI_DebugTools"
|
||||
ADDONS "!!!ClassicAPI" "ClassicAPI_DebugTools" "ClassicAPI_EventTrace"
|
||||
ADDON_VERSION "${CLASSICAPI_TOC_VERSION}"
|
||||
BINDINGS_XML "${CMAKE_CURRENT_SOURCE_DIR}/src/bindings/Bindings.xml"
|
||||
TEXTURES_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/src/embed/Textures"
|
||||
OUT_DIR "${CMAKE_BINARY_DIR}/generated")
|
||||
|
||||
target_compile_definitions(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# ClassicAPI
|
||||
|
||||
ClassicAPI is a DLL for World of Warcraft 1.12.1 (Vanilla / Turtle WoW). It
|
||||
backports a large part of the modern WoW API — 550+ functions and 50+ events,
|
||||
backports a large part of the modern WoW API — 950+ functions and 80+ events,
|
||||
plus much of Lua 5.1 — into the 1.12 client, so addons written for later
|
||||
versions (3.3.5+ or Classic Era) run with little or no change.
|
||||
|
||||
@@ -22,8 +22,9 @@ reference is in **[docs/API.md](docs/API.md)**.
|
||||
| Inline textures | Draws inline texture markup (`\|T…\|t`) and atlas markup (`\|A…\|a`) as icons in FontStrings, chat, and tooltips. This covers item and spell icons, raid-target markers, and the coin icons in money strings. `GetStringWidth` and `GetStringHeight` count the icons, so measured width and text wrapping stay correct. Done in pure C++ by hooking the engine's text pipeline — no addon. |
|
||||
| [Soft targeting](docs/API.md#soft-interact-softinteract--anyinteract) | The Interact Key and soft targets. Bind **Interact With Target** to a key to interact with the nearest NPC, corpse, mailbox, chest or gathering node in front of you. The [`softenemy` and `softfriend`](docs/API.md#soft-enemy-and-soft-friend-softenemy--softfriend--anyenemy--anyfriend) tokens point at the nearest enemy and friendly unit without a change of target. With no target, harmful spells go to `softenemy` and helpful spells go to `softfriend`. `PLAYER_SOFT_*_CHANGED` events fire when the units change. |
|
||||
| [Animations](docs/API.md#animation) | Animation groups on frames, textures and font strings, made in Lua or XML: `CreateAnimationGroup`, looping, smoothing, scripts and templates, with `Alpha`, `Translation`, `Scale`, `Rotation` and `Path` applied. |
|
||||
| [Model scenes](docs/API.md#modelscene) | The `ModelScene` frame type shows many models in one 3D scene, with its own camera, light and fog. Each model is an actor that you place, turn, scale and animate. An actor shows a creature display, a model file or a copy of a unit. It can also seat a rider, show spell visual kits and pause on one frame. A scene can show a WMO, for example a dungeon, around the actors, with its props and their sounds. An actor can move on the WMO as a character moves in the world: it runs, jumps, falls, swims and walks to a point. You hear the sounds of a scene from its camera, or from an actor. A follow camera follows an actor as the camera of the world follows your character, and stops in front of walls. An actor can glow, show a selection circle and show a name over its head, and a scene can show the click-to-move marker. |
|
||||
| Tooltip line cap | Removes `GameTooltip`'s 30-line limit for every `GameTooltipTemplate` frame (`GameTooltip`, `ShoppingTooltip1/2`, `ItemRefTooltip`, AtlasLoot, …). A tooltip adds a new line pair when it needs one, so long tooltips and comparison blocks (for example, pfUI's eqcompare) keep all their lines. |
|
||||
| [XML attributes](docs/API.md#parentkey-in-xml) | More XML attributes work: `parentKey="Name"` makes `parent.Name` point to a frame, texture or font string, `<OnLoad function="Name"/>` uses a global function as a script handler, `<OnLoad method="Name"/>` uses the frame's own function, `mixin="A, B"` copies mixins onto the object, `<KeyValues>` sets fields on the object, a button's `<NormalFont style="..."/>` sets its font, and `inherits="A, B"` applies more than one template. `parentArray="Name"` adds the object to a list on its parent. An `<Anchor>` takes `x` and `y` offsets and `relativeKey="$parent.Key"`. `<OnEnter inherit="prepend">` runs the template's handler and then its own, and `inherit="append"` runs them in the other order. |
|
||||
| [XML attributes](docs/API.md#parentkey-in-xml) | More XML attributes work: `parentKey="Name"` makes `parent.Name` point to a frame, texture or font string, `<OnLoad function="Name"/>` uses a global function as a script handler, `<OnLoad method="Name"/>` uses the frame's own function, `mixin="A, B"` copies mixins onto the object, `<KeyValues>` sets fields on the object, a button's `<NormalFont style="..."/>` sets its font, and `inherits="A, B"` applies more than one template. `parentArray="Name"` adds the object to a list on its parent. An `<Anchor>` takes `x` and `y` offsets and `relativeKey="$parent.Key"`. `<OnEnter inherit="prepend">` runs the template's handler and then its own, and `inherit="append"` runs them in the other order. A frame with `intrinsic="true"` makes a new frame type. Frames take `scale`, `registerForDrag`, `clipChildren`, `useParentLevel`, `dontSavePosition`, `propagateKeyboardInput`, `propagateMouseInput`, `enableMouseClicks`, `enableMouseMotion` and `motionScriptsWhileDisabled`, and buttons take `registerForClicks`. Textures take `alpha`, `atlas`, `useAtlasSize`, `mask`, `desaturated`, `rotation`, `horizTile`, `vertTile`, `hWrapMode` and `vWrapMode`. Font strings take `alpha`, `wordwrap`, `indented`, `degrees` and `radians`. Status bars take `reverseFill`, `rotatesTexture` and `fillStyle`, and edit boxes take `countInvisibleLetters`. |
|
||||
| [Macro conditions](docs/API.md#more-commands-with-conditions) | `[conditions]` and `@unit` work on 38 slash commands, from `/cast` and `/use` to `/target`, `/follow`, `/cancelaura` and the `/pet` family. With `/cast [@mouseover,harm][] Fireball`, the spell goes to the mouseover unit. With no mouseover unit, it goes to your target. `@cursor` places a ground-target spell or item under the mouse and `@player` places it at your feet. A character name works where a unit token does (`[target=Feral]`). Addons read the same rules through `SecureCmdOptionParse`. |
|
||||
| [`#showtooltip`](docs/API.md#showtooltip-and-show) | A macro button shows the spell or item that its directive resolves to, conditions included: icon, tooltip, cooldown, range, usable state and auto-repeat glow. Action-bar addons get this with no code of their own. Addons with their own macro parser can publish through `C_Macro.SetMacroDisplay`. |
|
||||
| [Event-driven nameplates](docs/API.md#nameplate) | The modern `C_NamePlate` API, driven by real events. `NAME_PLATE_UNIT_ADDED` / `NAME_PLATE_UNIT_REMOVED` fire as plates appear and vanish, `nameplate1`..`nameplateN` tokens resolve with every `UnitX` function (and fire `UNIT_HEALTH`, `UNIT_AURA`, … as `arg1 == "nameplateN"`), and `C_NamePlate.GetNamePlateForUnit` / `GetNamePlates` hand back the live frames. |
|
||||
@@ -45,17 +46,18 @@ heading to expand it. For signatures and return values, see the per-function
|
||||
reference in **[docs/API.md](docs/API.md)**.
|
||||
|
||||
<details>
|
||||
<summary><b>In-game Lua calls</b> — 550+ functions across ~60 namespaces</summary>
|
||||
<summary><b>In-game Lua calls</b> — 950+ functions across ~75 namespaces</summary>
|
||||
|
||||
In the Frame rows, a name that starts with `:` is a method of the widget that the row names, for example `:SetShown` is `frame:SetShown`.
|
||||
|
||||
| Namespace | Calls |
|
||||
|-----------|-------|
|
||||
| [Action](docs/API.md#action) | `GetActionInfo`, `IsItemAction`, `HasBonusActionBar` |
|
||||
| [Action](docs/API.md#action) | `GetActionInfo`, `IsItemAction`, `IsStackableAction`, `HasBonusActionBar`, `C_ActionBar.EnableActionRangeCheck`, `C_ActionBar.GetSpell`, `C_ActionBar.GetItemActionOnEquipSpellID`, `C_ActionBar.IsStackableAction`, `C_ActionBar.IsMacroActionWithShowTooltip`, `C_ActionBar.FindSpellActionButtons`, `C_ActionBar.HasSpellActionButtons`, `C_ActionBar.IsOnBarOrSpecialBar` |
|
||||
| [AddOns](docs/API.md#addons) | `C_AddOns.DoesAddOnExist`, `C_AddOns.GetAddOnInterfaceVersion`, `C_AddOns.GetAddOnLocalTable`, `C_AddOns.GetAddOnName`, `C_AddOns.GetAddOnNotes`, `C_AddOns.GetAddOnOptionalDependencies`, `C_AddOns.GetAddOnSecurity`, `C_AddOns.GetAddOnTitle`, `C_AddOns.IsAddOnLoadable`, `C_AddOns.IsAddOnLoaded`, `C_AddOns.LoadAddOn` |
|
||||
| [APIDocumentation](docs/API.md#apidocumentation) | `/classicapi` in-game browser, `C_APIDocumentation.GetSystem`, `C_APIDocumentation.GetSystems` |
|
||||
| [Animation](docs/API.md#animation) | `region:CreateAnimationGroup`, `region:GetAnimationGroups`, `region:StopAnimating`, `SetMaxAnimFramerate`, `GetMaxAnimFramerate`, `AnimationGroup` and `Animation` / `Alpha` / `Translation` / `Scale` / `Rotation` / `Path` / `ControlPoint` methods |
|
||||
| [AuctionHouse](docs/API.md#auctionhouse) | `C_AuctionHouse.PostItem`, `CanCancelAuction` |
|
||||
| [AutoComplete](docs/API.md#autocomplete) | `C_AutoComplete.GetAutoCompleteResults`, `C_AutoComplete.IsRecognizedName`, `C_AutoComplete.GetAutoCompletePresenceID`, `C_AutoComplete.GetAutoCompleteRealms` |
|
||||
| [Bindings](docs/API.md#bindings) | `SetBindingSpell`, `SetBindingItem`, `SetBindingMacro`, `SetBindingClick`, `SetOverrideBinding`, `SetOverrideBindingSpell`, `SetOverrideBindingItem`, `SetOverrideBindingMacro`, `SetOverrideBindingClick`, `ClearOverrideBindings`, `GetBindingAction` (`checkOverride`), `GetBindingByKey`, `IsModifiedClick`, `GetModifiedClick`, `SetModifiedClick`, `GetNumModifiedClickActions`, `GetModifiedClickAction` |
|
||||
| [Chat](docs/API.md#chat) | `GetCurrentChatEvent`, `GetCurrentChatGUID`, `IsChatAFK`, `IsChatDND`, `SendSystemMessage` |
|
||||
| [ChatBubbles](docs/API.md#chatbubbles) | `C_ChatBubbles.GetAllChatBubbles` |
|
||||
@@ -69,23 +71,26 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
|
||||
| [Cursor](docs/API.md#cursor) | `CursorHasMacro`, `GetCursorInfo` |
|
||||
| [Container](docs/API.md#container) | `C_Container.AutoStoreItem`, `C_Container.CalculateTotalNumberOfFreeBagSlots`, `C_Container.GetContainerItemCharges`, `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemInfo`, `C_Container.GetContainerItemEquipmentSetInfo`, `C_Container.GetContainerItemQuestInfo`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerFreeSlots`, `C_Container.GetContainerNumFreeSlots`, `C_Container.GetBackpackAutosortDisabled`, `C_Container.GetBankAutosortDisabled`, `C_Container.GetItemCooldown`, `C_Container.GetSortBagsRightToLeft`, `C_Container.HasContainerItem`, `C_Container.IsContainerFiltered`, `C_Container.IsContainerItemOpenable`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SetBackpackAutosortDisabled`, `C_Container.SetBagPortraitTexture`, `C_Container.SetBankAutosortDisabled`, `C_Container.SetItemSearch`, `C_Container.SetSortBagsRightToLeft`, `C_Container.SortBags`, `C_Container.SortBankBags`, `C_Container.SwapItems`, `C_Container.UseHearthstone`, `GetItemCooldown`, `SetBagPortraitTexture` |
|
||||
| [Creature](docs/API.md#creature) | `C_CreatureInfo.GetCreatureID`, `C_CreatureInfo.GetCreatureInfoByID`, `C_CreatureInfo.RequestLoadCreatureByID`, `C_CreatureInfo.GetRaceInfo`, `C_CreatureInfo.GetClassInfo`, `C_CreatureInfo.GetCreatureFamilyInfo`, `C_CreatureInfo.GetCreatureFamilyIDs`, `C_CreatureInfo.GetFactionInfo`, `C_CreatureInfo.GetCreatureTypeInfo`, `C_CreatureInfo.GetCreatureTypeIDs` |
|
||||
| [Currency](docs/API.md#currency) | `GetCoinText`, `C_CurrencyInfo.GetCoinText`, `GetCoinTextureString`, `C_CurrencyInfo.GetCoinTextureString` |
|
||||
| [Currency](docs/API.md#currency) | `GetCoinText`, `C_CurrencyInfo.GetCoinText`, `GetCoinTextureString`, `C_CurrencyInfo.GetCoinTextureString`, `Constants.MoneyFormattingConstants` |
|
||||
| EncodingUtil | `C_EncodingUtil.CompressString`, `C_EncodingUtil.DecompressString`, `C_EncodingUtil.EncodeBase64`, `C_EncodingUtil.DecodeBase64`, `C_EncodingUtil.EncodeHex`, `C_EncodingUtil.DecodeHex`, `C_EncodingUtil.SerializeJSON`, `C_EncodingUtil.DeserializeJSON`, `C_EncodingUtil.SerializeCBOR`, `C_EncodingUtil.DeserializeCBOR` |
|
||||
| [DeathInfo](docs/API.md#deathinfo) | `C_DeathInfo.GetSelfResurrectOptions`, `C_DeathInfo.UseSelfResurrectOption` |
|
||||
| [DurationUtil](docs/API.md#durationutil) | `C_DurationUtil.CreateDuration`, `C_DurationUtil.CreateDurationTextBinding` |
|
||||
| [EquipmentSet](docs/API.md#equipmentset) | `C_EquipmentSet.CanUseEquipmentSets`, `C_EquipmentSet.ClearIgnoredSlotsForSave`, `C_EquipmentSet.CreateEquipmentSet`, `C_EquipmentSet.DeleteEquipmentSet`, `C_EquipmentSet.EquipmentSetContainsLockedItems`, `C_EquipmentSet.GetEquipmentSetID`, `C_EquipmentSet.GetEquipmentSetIDs`, `C_EquipmentSet.GetEquipmentSetInfo`, `C_EquipmentSet.GetIgnoredSlots`, `C_EquipmentSet.GetItemIDs`, `C_EquipmentSet.GetItemLocations`, `C_EquipmentSet.GetNumEquipmentSets`, `C_EquipmentSet.IgnoreSlotForSave`, `C_EquipmentSet.IsSlotIgnoredForSave`, `C_EquipmentSet.ModifyEquipmentSet`, `C_EquipmentSet.PickupEquipmentSet`, `C_EquipmentSet.SaveEquipmentSet`, `C_EquipmentSet.UnignoreSlotForSave`, `C_EquipmentSet.UseEquipmentSet` |
|
||||
| [Events](docs/API.md#events) | `C_EventUtils.IsEventValid`, `GetFramesRegisteredForEvent` |
|
||||
| [Expansion](docs/API.md#expansion) | `ClassicExpansionAtLeast`, `ClassicExpansionAtMost`, `GetAccountExpansionLevel`, `GetClassicExpansionLevel`, `GetExpansionLevel`, `GetMaxPlayerLevel` |
|
||||
| [Faction](docs/API.md#faction) | `C_Reputation.GetFactionDataByID`, `C_Reputation.GetFactionDataByIndex`, `C_Reputation.GetFactionStandings`, `C_Reputation.GetLastStandingChange`, `C_Reputation.GetWatchedFactionData`, `C_Reputation.IsFactionActive`, `C_Reputation.IsFactionActiveByID`, `C_Reputation.IsFactionInactive`, `C_Reputation.SetFactionActive`, `C_Reputation.SetFactionActiveByID`, `C_Reputation.SetFactionInactive`, `C_Reputation.SetFactionInactiveByID`, `C_Reputation.SetSelectedFaction`, `C_Reputation.SetSelectedFactionByID`, `C_Reputation.SetWatchedFactionByID`, `C_Reputation.ToggleFactionAtWar`, `C_Reputation.ToggleFactionAtWarByID`, `CollapseAllFactionHeaders`, `ExpandAllFactionHeaders`, `GetFactionIDByIndex`, `GetFactionInfoByID`, `GetFactionParentID` |
|
||||
| [Focus](docs/API.md#focus) | `ClearFocus`, `FocusUnit` |
|
||||
| [Frame: Region](docs/API.md#regionsetpointpoint-one-argument-form) | `:SetPoint("point")` (one-arg form), `:SetSize` / `:GetSize`, `ignoreRect` on `:GetSize` / `:GetWidth` / `:GetHeight`, `:GetRect`, `:GetScaledRect`, `:IsRectValid`, `:GetPointByName`, `:ClearPoint`, `:GetScale` / `:GetEffectiveScale` (on textures and font strings), `:GetDebugName`, `:IsMouseOver`, `:IsDragging`, `:IsProtected` / `:CanChangeProtectedState` |
|
||||
| [Frame: Region](docs/API.md#regionsetpointpoint-one-argument-form) | `:SetPoint("point")` (one-arg form), `:SetSize` / `:GetSize`, `ignoreRect` on `:GetSize` / `:GetWidth` / `:GetHeight`, `:GetUnscaledWidth` / `:GetUnscaledHeight` / `:GetUnscaledSize`, `:GetRect`, `:GetScaledRect`, `:IsRectValid`, `:GetPointByName`, `:ClearPoint`, `:GetScale` / `:GetEffectiveScale` (on textures and font strings), `:GetDebugName`, `:IsMouseOver`, `:IsDragging`, `:IsProtected` / `:CanChangeProtectedState` |
|
||||
| [Frame: Frame](docs/API.md#createframeframetype--name-parent-template-id) | `CreateFrame` template lists and `id`, `GetMouseFoci`, `:SetShown`, `:SetClampRectInsets` / `:GetClampRectInsets`, `:SetResizeBounds`, `:SetClipsChildren` / `:DoesClipChildren`, `:GetBoundsRect`, `:GetEffectiveAlpha`, `:SetAlphaGradient` / `:ClearAlphaGradient` / `:HasAlphaGradient`, `:DesaturateHierarchy`, `:HookScript`, `:IsEventRegistered`, `:RegisterUnitEvent`, `:SetPropagateKeyboardInput` / `:GetPropagateKeyboardInput`, `:SetDontSavePosition` / `:GetDontSavePosition`, `:SetAttribute`, `:SetAttributeNoHandler`, `:ClearAttribute`, `:GetAttribute`, `:CanChangeAttribute`, `:AllowAttributeChanges` |
|
||||
| [Frame: Button](docs/API.md#buttonsetnormalfontobjectfont--buttongetnormalfontobject) | `:SetEnabled`, `:SetFormattedText`, `:SetNormalFontObject` / `:GetNormalFontObject`, `:SetMotionScriptsWhileDisabled` / `:GetMotionScriptsWhileDisabled`, `:RegisterForClicks("AnyUp"/"AnyDown")`, `:RegisterForMouse`, `GetClickFrame` |
|
||||
| [Frame: FontString](docs/API.md#fontstringgetstringheight) | `:GetStringHeight`, `:GetUnboundedStringWidth`, `:GetWrappedWidth`, `:GetNumLines`, `:GetLineHeight`, `:GetFontHeight` / `:SetFontHeight` (also on fonts), `:GetFieldSize`, `:IsTruncated`, `:SetMaxLines` / `:GetMaxLines`, `:SetWordWrap` / `:CanWordWrap`, `:SetIndentedWordWrap` / `:GetIndentedWordWrap` (also on fonts, edit boxes, message frames, and SimpleHTML), `:SetFormattedText`, `:SetRotation` / `:GetRotation` |
|
||||
| [Frame: Texture](docs/API.md#texturesetrotationangle--cx-cy) | `:SetRotation` / `:GetRotation`, `:SetVertexOffset` / `:GetVertexOffset`, `:SetColorTexture`, `:SetAtlas` / `:GetAtlas`, `:ResetTexCoord`, `:SetSpriteSheetCell`, `:SetDesaturation` / `:GetDesaturation`, `IsDesaturateSupported`, `:SetMask`, `frame:CreateMaskTexture`, `:AddMaskTexture` / `:RemoveMaskTexture`, `:GetNumMaskTextures`, `:GetMaskTexture` |
|
||||
| [Frame: Texture](docs/API.md#texturesetrotationangle--cx-cy) | `:SetRotation` / `:GetRotation`, `:SetVertexOffset` / `:GetVertexOffset`, `:SetColorTexture`, `:SetAtlas` / `:GetAtlas`, `:ResetTexCoord`, `:SetSpriteSheetCell`, `:SetTexture` wrap modes, `:SetHorizTile` / `:GetHorizTile`, `:SetVertTile` / `:GetVertTile`, `:SetDesaturation` / `:GetDesaturation`, `IsDesaturateSupported`, `:SetMask`, `frame:CreateMaskTexture`, `:AddMaskTexture` / `:RemoveMaskTexture`, `:GetNumMaskTextures`, `:GetMaskTexture` |
|
||||
| [Frame: EditBox](docs/API.md#editboxsetcursorpositionposition) | `:SetCursorPosition` / `:GetCursorPosition`, `:GetUTF8CursorPosition`, `:ClearHighlightText`, `:SetHighlightColor` / `:GetHighlightColor`, `:HasFocus`, `GetCurrentKeyBoardFocus`, `:HasText`, `:ClearHistory`, `:Enable` / `:Disable` / `:IsEnabled`, `:SetCountInvisibleLetters` / `:IsCountInvisibleLetters`, `:IsInIMECompositionMode` |
|
||||
| [Frame: StatusBar](docs/API.md#statusbarsetreversefillisreversefill--statusbargetreversefill) | `:SetReverseFill` / `:GetReverseFill`, `:SetRotatesTexture` / `:GetRotatesTexture`, `:SetFillStyle` / `:GetFillStyle`, `:SetColorFill`, `:SetStatusBarDesaturated` / `:IsStatusBarDesaturated`, `:SetStatusBarDesaturation` / `:GetStatusBarDesaturation` |
|
||||
| [Frame: StatusBar](docs/API.md#statusbarsetreversefillisreversefill--statusbargetreversefill) | `:SetReverseFill` / `:GetReverseFill`, `:SetRotatesTexture` / `:GetRotatesTexture`, `:SetFillStyle` / `:GetFillStyle`, `:SetColorFill`, `:SetStatusBarDesaturated` / `:IsStatusBarDesaturated`, `:SetStatusBarDesaturation` / `:GetStatusBarDesaturation`, `:SetTimerDuration` / `:GetTimerDuration`, `:SetValue` / `:SetMinMaxValues` with `interpolation`, `:GetInterpolatedValue`, `:IsInterpolating`, `:SetToTargetValue` |
|
||||
| [Frame: Slider](docs/API.md#sliderenable--sliderdisable--sliderisenabled) | `:Enable` / `:Disable` / `:IsEnabled` |
|
||||
| [Frame: Cooldown](docs/API.md#createframecooldown--name-parent-template) | `CreateFrame("Cooldown")`, `:SetCooldown`, `:SetCooldownDuration`, `:SetCooldownFromExpirationTime`, `:Clear`, `:GetCooldownTimes`, `:GetCooldownDuration`, `:GetCooldownDisplayDuration`, `:Pause` / `:Resume`, `:SetPaused` / `:IsPaused`, `:SetReverse` / `:GetReverse`, `:SetDrawEdge` / `:GetDrawEdge`, `:SetDrawSwipe` / `:GetDrawSwipe`, `:SetDrawBling` / `:GetDrawBling`, `:SetSwipeColor`, `:SetEdgeColor`, `:SetSwipeTexture`, `:SetEdgeTexture`, `:SetBlingTexture`, `:SetRotation` / `:GetRotation`, `:SetHideCountdownNumbers` / `:GetHideCountdownNumbers`, `:GetCountdownFontString`, `:SetCountdownAbbrevThreshold` / `:GetCountdownAbbrevThreshold`, `:SetCountdownMillisecondsThreshold` / `:GetCountdownMillisecondsThreshold`, `:SetMinimumCountdownDuration` / `:GetMinimumCountdownDuration`, `OnCooldownDone`, `CooldownFrame_Set`, `CooldownFrame_Clear`, `CooldownFrame_SetDisplayAsPercentage` |
|
||||
| [Frame: ModelScene](docs/API.md#modelscene) | `CreateFrame("ModelScene")`, `<ModelScene>` (`drawLayer`, `fogNear`, `fogFar`, `<FogColor>`, `<ViewInsets>`), `:CreateActor`, `:GetNumActors`, `:GetActorAtIndex`, `:SetCameraPosition` / `:GetCameraPosition`, `:SetCameraOrientationByYawPitchRoll`, `:SetCameraOrientationByAxisVectors`, `:GetCameraForward`, `:GetCameraRight`, `:GetCameraUp`, `:SetCameraFieldOfView` / `:GetCameraFieldOfView`, `:SetCameraNearClip` / `:GetCameraNearClip`, `:SetCameraFarClip` / `:GetCameraFarClip`, `:SetViewInsets` / `:GetViewInsets`, `:SetViewTranslation` / `:GetViewTranslation`, `:Project3DPointTo2D`, `"FollowCamera"` (`:SetFollowActor` / `:GetFollowActor`), `:SetLightType` / `:GetLightType`, `:SetLightVisible` / `:IsLightVisible`, `:SetLightDirection` / `:GetLightDirection`, `:SetLightPosition` / `:GetLightPosition`, `:SetLightAmbientColor` / `:GetLightAmbientColor`, `:SetLightDiffuseColor` / `:GetLightDiffuseColor`, `:SetFogColor` / `:GetFogColor`, `:SetFogNear` / `:GetFogNear`, `:SetFogFar` / `:GetFogFar`, `:ClearFog`, `:SetDrawLayer` / `:GetDrawLayer`, `:SetPaused`, `:GetSceneTime`, `:After`, `:NewTimer`, `:NewTicker`, `:SetAllowOverlappedModels` / `:GetAllowOverlappedModels`, `:SetGroundTerrain` / `:GetGroundTerrain`, `:SetWorldModel`, `:SetWorldModelByMapID`, `:ClearWorldModel`, `:SetWorldTerrain`, `:ClearWorldTerrain`, `:SetPlayWorldModelSounds` / `:GetPlayWorldModelSounds`, `:TraceSegment`, `:GetLiquidAtPoint`, `:SweepCollider`, `:SetSoundListenerActor` / `:GetSoundListenerActor`, `:GetActorAtCursor`, `:GetCursorRay`, `:GetActorsInRadius`, `:SetMoveMarker` / `:ClearMoveMarker` / `:GetMoveMarker`, `OnActorEnter` / `OnActorLeave` |
|
||||
| [Frame: ModelSceneActor](docs/API.md#actorsetmodelbycreaturedisplayiddisplayid--useactiveplayercustomizations) | `:SetModelByCreatureDisplayID`, `:SetModelByPath`, `:SetModelByUnit`, `:SetModelByUnitCreatureDisplayID`, `:GetModelUnitGUID`, `:Dress`, `:Undress`, `:UndressSlot`, `:TryOn`, `:DressPlayerSlot`, `:IsSlotAllowed` / `:IsSlotVisible`, `:GetItemTransmogInfo`, `:GetItemTransmogInfoList`, `:SetItemTransmogInfo`, `:SetSheathed` / `:GetSheathed`, `:SetSheathedCategory`, `:UseUnitSheatheCategory`, `:SetAutoDress` / `:GetAutoDress`, `:IsGeoReady`, `:ClearModel`, `:IsLoaded`, `:GetModelPath`, `:SetPosition` / `:GetPosition`, `:SetYaw` / `:GetYaw`, `:SetPitch` / `:GetPitch`, `:SetRoll` / `:GetRoll`, `:SetScale` / `:GetScale`, `:SetUseCenterForOrigin` / `:IsUsingCenterForOrigin`, `:GetActiveBoundingBox`, `:GetMaxBoundingBox`, `:Show` / `:Hide` / `:SetShown` / `:IsShown` / `:IsVisible`, `:SetAlpha` / `:GetAlpha`, `:SetAnimation` / `:GetAnimation`, `:GetAnimationVariation`, `:SetAnimationBlendOperation` / `:GetAnimationBlendOperation`, `:SetPaused` / `:GetPaused`, `:SetSpellVisualKit` / `:GetSpellVisualKit`, `:ApplySpellVisualKit`, `:PlayAnimationKit` / `:StopAnimationKit`, `:AttachToMount`, `:DetachFromMount`, `:CalculateMountScale`, `:GetAttachmentPosition`, `:AttachToActor`, `:DetachFromActor`, `:PlaySpellVisual`, `:StopSpellVisual`, `:PlaySound`, `:SetPlayAnimationSounds` / `:GetPlayAnimationSounds`, `:SetShadowShown` / `:IsShadowShown`, `:SetHighlight` / `:IsHighlighted`, `:SetSelectionCircle` / `:ClearSelectionCircle` / `:GetSelectionCircle`, `:SetNameText` / `:ClearNameText` / `:GetNameText`, `:GetNamePosition`, `:SetMovementEnabled` / `:IsMovementEnabled`, `:StartMovingForward`, `:StartMovingBackward`, `:StopMoving`, `:StartStrafingLeft`, `:StartStrafingRight`, `:StopStrafing`, `:StartTurningLeft`, `:StartTurningRight`, `:StopTurning`, `:Jump`, `:StartPitchingUp`, `:StartPitchingDown`, `:StopPitching`, `:IsFalling`, `:IsSwimming`, `:MoveTo`, `:SetWalking` / `:IsWalking`, `:GetCameraPivotHeight`, `OnLoad`, `OnUpdate`, `OnModelLoading`, `OnModelLoaded`, `OnModelCleared`, `OnAnimFinished`, `OnSpellVisualImpact`, `OnMoveFinished`, `<Actor>` templates |
|
||||
| [Frame: ScrollingMessageFrame](docs/API.md#smfsethyperlinksenabledenabled--smfgethyperlinksenabled) | `:SetHyperlinksEnabled` / `:GetHyperlinksEnabled`, `:GetMessageInfo` (with the extra `:AddMessage` values), `:BackFillMessage` (adds a message as the oldest), `:RemoveMessagesByPredicate`, `:SetScrollOffset` / `:GetScrollOffset`, `:GetMaxScrollRange`, `:ScrollByAmount`, `:SetInsertMode` / `:GetInsertMode`, `:AddOnDisplayRefreshedCallback` |
|
||||
| [Frame: scripts](docs/API.md#onenable-and-ondisable-scripts) | `OnEnable`, `OnDisable`, `OnMinMaxChanged`, `OnAttributeChanged`, `OnCharComposition`, `OnTextChanged` (`userInput`), `PreClick`, `PostClick`, `SetModernScriptArgs` / `GetModernScriptArgs` |
|
||||
| [FriendList](docs/API.md#friendlist) | `AddOrRemoveFriend`, `C_FriendList.GetFriendInfo`, `C_FriendList.GetFriendInfoByIndex`, `C_FriendList.GetNumFriends`, `C_FriendList.GetNumOnlineFriends`, `C_FriendList.GetNumWhoResults`, `C_FriendList.GetWhoInfo`, `C_FriendList.IsFriend`, `C_FriendList.IsIgnored`, `C_FriendList.IsIgnoredByGuid`, `C_FriendList.IsWhoQueryPending`, `C_FriendList.SendWhoQueryByName`, `C_FriendList.SetFriendNotes`, `C_FriendList.SetFriendNotesByIndex` |
|
||||
@@ -94,22 +99,25 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
|
||||
| [GameTooltip](docs/API.md#gametooltip) | `GameTooltip:AddSpellByID`, `GameTooltip:AddTexture`, `GameTooltip:GetGameObject`, `GameTooltip:GetItem`, `GameTooltip:GetMinimumWidth`, `GameTooltip:GetOwner`, `GameTooltip:GetPadding`, `GameTooltip:GetSpell`, `GameTooltip:GetUnit`, `GameTooltip:GetUnitGUID`, `GameTooltip:HasGameObject`, `GameTooltip:HasItem`, `GameTooltip:HasSpell`, `GameTooltip:HasUnit`, `GameTooltip:IsEquippedItem`, `GameTooltip:IsUnit`, `GameTooltip:SetAnchorType`, `GameTooltip:SetEquipmentSet`, `GameTooltip:SetFrameStack`, `GameTooltip:SetHyperlink` (full links, `spell:`, `quest:`), `GameTooltip:SetHyperlinkCompareItem`, `GameTooltip:SetInventoryItemByID`, `GameTooltip:SetItemByGUID`, `GameTooltip:SetItemByID`, `GameTooltip:SetSpellByID`, `GameTooltip:SetTalentByID`, `GameTooltip:SetTotem`, `GameTooltip:SetUnitAura`, `OnTooltipSetItem`, `OnTooltipSetSpell`, `OnTooltipSetUnit`, `OnTooltipSetGameObject`, `OnTooltipSetFrameStack` (scripts) |
|
||||
| [Gossip](docs/API.md#gossip) | `C_GossipInfo.CloseGossip`, `C_GossipInfo.GetActiveQuests`, `C_GossipInfo.GetAvailableQuests`, `C_GossipInfo.GetNumActiveQuests`, `C_GossipInfo.GetNumAvailableQuests`, `C_GossipInfo.GetNumOptions`, `C_GossipInfo.GetOptions`, `C_GossipInfo.GetPoiForUiMapID`, `C_GossipInfo.GetPoiInfo`, `C_GossipInfo.GetText`, `C_GossipInfo.SelectActiveQuest`, `C_GossipInfo.SelectAvailableQuest`, `C_GossipInfo.SelectOption`, `C_GossipInfo.SelectOptionByIndex`, `GetNumGossipActiveQuests`, `GetNumGossipAvailableQuests`, `GetNumGossipOptions` |
|
||||
| [Hooks](docs/API.md#hooks) | `hooksecurefunc` |
|
||||
| [Input](docs/API.md#input) | `GetMouseButtonClicked`, `GetMouseButtonName`, `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsMouseButtonDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
|
||||
| [Input](docs/API.md#input) | `GetCursorDelta`, `GetMouseButtonClicked`, `GetMouseButtonName`, `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsMouseButtonDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
|
||||
| [Instance](docs/API.md#instance) | `GetInstanceInfo`, `IsActiveBattlefieldArena` |
|
||||
| [Item: C_Item](docs/API.md#item) | `C_Item.DoesItemExist` / `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetEnchantInfo`, `C_Item.GetItemClassInfo`, `C_Item.GetItemCount`, `C_Item.GetItemData` / `C_Item.GetItemDataByID`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon` / `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfo`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventorySlotInfo`, `C_Item.GetItemInventorySlotKey`, `C_Item.GetItemInventoryType` / `C_Item.GetItemInventoryTypeByID`, `C_Item.GetItemLink`, `C_Item.GetItemLocation`, `C_Item.GetItemMaxStackSize` / `C_Item.GetItemMaxStackSizeByID`, `C_Item.GetItemName` / `C_Item.GetItemNameByID`, `C_Item.GetItemQuality` / `C_Item.GetItemQualityByID`, `C_Item.GetItemSellPrice` / `C_Item.GetItemSellPriceByID`, `C_Item.GetItemSetID` / `C_Item.GetItemSetIDByID`, `C_Item.GetItemSetInfo`, `C_Item.GetItemSpell`, `C_Item.GetItemStatDelta`, `C_Item.GetItemStats`, `C_Item.GetItemSubClassInfo`, `C_Item.GetItemTempEnchantInfo`, `C_Item.GetItemUniqueness` / `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsConsumableItem`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached` / `C_Item.IsItemDataCachedByID`, `C_Item.IsItemGUIDInInventory`, `C_Item.IsItemInRange`, `C_Item.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.IsUsableItem`, `C_Item.ItemHasRange`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.UnlockItem`, `C_Item.UnlockAllItems`, `C_Item.PickupItem`, `C_Item.RequestLoadItemData` / `C_Item.RequestLoadItemDataByID`, `C_Item.UseAtCursor`, `C_Item.UseAtUnit`, `C_Item.UseItemByName` |
|
||||
| [Item: info](docs/API.md#getitemclassinfoclassid) | `GetItemClassInfo`, `GetItemSubClassInfo`, `GetItemIcon`, `IsConsumableItem`, `IsCurrentItem`, `IsDressableItem`, `IsEquippedItemType`, `IsHarmfulItem`, `IsHelpfulItem`, `IsUsableItem` |
|
||||
| [Item: inventory](docs/API.md#getaverageitemlevel) | `GetAverageItemLevel`, `GetInventoryItemID`, `GetInventoryItemDurability`, `GetInventoryItemRepairCost`, `GetInventoryItemsForSlot`, `IsDualWielding`, `SetInventoryPortraitTexture` |
|
||||
| [Item: item IDs](docs/API.md#getitemid--companions-to-the-engines-getitemlink-family) | `GetAuctionItemID`, `GetAuctionSellItemID`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeTargetItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID` |
|
||||
| [Localization](docs/API.md#localization) | `AbbreviateLargeNumbers`, `AbbreviateNumbers`, `BreakUpLargeNumbers`, `C_StringUtil.GetDefaultAbbreviationBreakpoints` |
|
||||
| [Loot](docs/API.md#loot) | `C_Loot.GetNearbyLootableUnits`, `C_Loot.GetLastScanResults`, `C_Loot.IsScanInProgress`, `C_Loot.LootAllCorpses`, `C_Loot.LootUnit`, `C_Loot.LootUnitItem`, `C_Loot.ScanNearbyLoot` |
|
||||
| [LootHistory](docs/API.md#loothistory) | `C_LootHistory.GetNumItems`, `C_LootHistory.GetItem`, `C_LootHistory.GetPlayerInfo`, `C_LootHistory.Clear` |
|
||||
| [LossOfControl](docs/API.md#lossofcontrol) | `C_LossOfControl.GetActiveLossOfControlData`, `C_LossOfControl.GetActiveLossOfControlDataCount`, `C_LossOfControl.GetSchoolLockout` |
|
||||
| [Lua](docs/API.md#lua) | `collectgarbage` (5.1 options), `coroutine.create`, `coroutine.resume`, `coroutine.running`, `coroutine.status`, `coroutine.wrap`, `coroutine.yield`, `CreateFromMixins`, `math.fmod`, `math.huge`, `math.modf`, `Mixin`, `select`, `strcmputf8i`, `strconcat`, `string.concat`, `string.gmatch`, `string.gsub` (table replacement), `string.join`, `string.match`, `string.reverse`, `string.split`, `string.trim`, `strjoin`, `strlenutf8`, `strmatch`, `strreplace`, `strrev`, `strsplit`, `strsplittable`, `strtrim`, `table.count`, `table.maxn`, `table.wipe`, `unpack` (range args), `xpcall` (argument forwarding) |
|
||||
| [Lua](docs/API.md#lua) | `collectgarbage` (5.1 options), `coroutine.create`, `coroutine.resume`, `coroutine.running`, `coroutine.status`, `coroutine.wrap`, `coroutine.yield`, `CreateFromMixins`, `debuglocals`, `debugstack` (coroutine argument), `difftime`, `GetCallstackHeight`, `GetErrorCallstackHeight`, `gmatch`, `math.cosh`, `math.fmod`, `math.huge`, `math.modf`, `math.sinh`, `math.tanh`, `Mixin`, `newproxy`, `scrub`, `select`, `SetErrorCallstackHeight`, `strbyte` (range args), `strcmputf8i`, `strconcat`, `string.byte` (range args), `string.concat`, `string.format` (two-digit `%n$`), `string.gmatch`, `string.gsub` (table replacement, `%0`), `string.join`, `string.match`, `string.reverse`, `string.split`, `string.trim`, `strjoin`, `strlenutf8`, `strmatch`, `strreplace`, `strrev`, `strsplit`, `strsplittable`, `strtrim`, `table.count`, `table.maxn`, `table.removemulti`, `table.wipe`, `unpack` (range args), `wipe`, `xpcall` (argument forwarding) |
|
||||
| [Macros](docs/API.md#macros) | `C_Macro.CreateMacro`, `C_Macro.EditMacro`, `C_Macro.SetMacroDisplay`, `DeleteMacro` / `EditMacro` / `GetMacroInfo` / `PickupMacro` by name, `GetLooseMacroIcons`, `GetLooseMacroItemIcons`, `GetMacroBody`, `GetMacroIcons`, `GetMacroItem`, `GetMacroItemIcons`, `GetMacroSpell`, `GetRunningMacro`, `GetRunningMacroButton`, `RunMacroText`, `SetMacroItem`, `SetMacroSpell`, `StopMacro` |
|
||||
| [Mail](docs/API.md#mail) | `GetInboxItemLink`, `GetSendMailItemLink` |
|
||||
| [Map](docs/API.md#map) | `C_Map.CanSetUserWaypointOnMap`, `C_Map.ClearUserWaypoint`, `C_Map.GetAreaInfo`, `C_Map.GetAreas`, `C_Map.GetAreaTriggerInfo`, `C_Map.GetAreaTriggers`, `C_Map.GetBestMapForUnit`, `C_Map.GetFallbackWorldMapID`, `C_Map.GetMapAreaIDs`, `C_Map.GetMapArtLayers`, `C_Map.GetMapArtLayerTextures`, `C_Map.GetMapChildrenInfo`, `C_Map.GetMapInfo`, `C_Map.GetMapInfoAtPosition`, `C_Map.GetMapOverlays`, `C_Map.GetMapPosFromWorldPos`, `C_Map.GetMapRectOnMap`, `C_Map.GetMapWorldSize`, `C_Map.GetPlayerMapPosition`, `C_Map.GetUserWaypoint`, `C_Map.GetUserWaypointFromHyperlink`, `C_Map.GetUserWaypointHyperlink`, `C_Map.GetUserWaypointPositionForMap`, `C_Map.GetWorldPosFromMapPos`, `C_Map.HasUserWaypoint`, `C_Map.MapHasArt`, `C_Map.SetUserWaypoint` |
|
||||
| [MapExplorationInfo](docs/API.md#mapexplorationinfo) | `C_MapExplorationInfo.GetExploredAreaIDsAtPosition`, `C_MapExplorationInfo.GetExploredMapTextures`, `C_MapExplorationInfo.GetUnexploredMapTextures` |
|
||||
| [MerchantFrame](docs/API.md#merchantframe) | `C_MerchantFrame.GetBuybackItemID`, `C_MerchantFrame.GetItemInfo`, `C_MerchantFrame.GetNumJunkItems`, `C_MerchantFrame.IsMerchantItemRefundable`, `C_MerchantFrame.IsSellAllJunkEnabled`, `C_MerchantFrame.SellAllJunkItems`, `GetBuybackItemLink` |
|
||||
| [Model](docs/API.md#model) | `DressUpModel:TryOn` (links, recipes, names), `DressUpModel:UndressSlot`, `Model:SetGlow`, `PlayerModel:RefreshCamera`, `PlayerModel:SetCamDistanceScale`, `PlayerModel:SetCreature`, `PlayerModel:SetDisplayInfo`, `PlayerModel:SetPortraitZoom`, `PlayerModel:SetRotation` (`animate`) |
|
||||
| [Model](docs/API.md#model) | `DressUpModel:TryOn` (links, recipes, names, hand, enchant glow, result), `DressUpModel:UndressSlot`, `DressUpModel:IsGeoReady`, `DressUpModel:SetSheathed` / `GetSheathed`, `DressUpModel:SetAutoDress` / `GetAutoDress`, `DressUpModel:GetItemTransmogInfo` / `GetItemTransmogInfoList` / `SetItemTransmogInfo`, `DressUpModel:IsSlotAllowed` / `IsSlotVisible`, `Model:SetGlow`, `PlayerModel:RefreshCamera`, `PlayerModel:SetCamDistanceScale`, `PlayerModel:SetCreature` (`displayID`), `PlayerModel:SetDisplayInfo`, `PlayerModel:SetItem`, `PlayerModel:SetPortraitZoom`, `PlayerModel:SetRotation` (`animate`), `PlayerModel:SetUnit` (result), `PlayerModel:CanSetUnit`, `PlayerModel:GetDisplayInfo`, `PlayerModel:SetAnimation`, `PlayerModel:FreezeAnimation`, `PlayerModel:HasAnimation`, `PlayerModel:SetDoBlend` / `GetDoBlend`, `PlayerModel:SetKeepModelOnHide` / `GetKeepModelOnHide`, `PlayerModel:ApplySpellVisualKit` |
|
||||
| [ModelInfo](docs/API.md#c_modelinfogetmodelsceneinfobyidmodelsceneid) | `C_ModelInfo.GetModelSceneInfoByID`, `C_ModelInfo.GetModelSceneCameraInfoByID`, `C_ModelInfo.GetModelSceneActorInfoByID`, `C_ModelInfo.GetModelSceneActorDisplayInfoByID` |
|
||||
| [Mount](docs/API.md#mount) | `C_MountJournal.GetMountFromItem`, `C_MountJournal.GetMountFromSpell`, `C_MountJournal.GetMountInfoExtraByID` |
|
||||
| [NamePlate](docs/API.md#nameplate) | `C_NamePlate.GetNamePlateForGUID`, `C_NamePlate.GetNamePlateForUnit`, `C_NamePlate.GetNamePlateGUIDs`, `C_NamePlate.GetNamePlates` |
|
||||
| [NameCache](docs/API.md#namecache) | `C_PlayerCache.GetPlayerInfoByName`, `C_PlayerCache.IsEnabled`, `C_PlayerCache.RememberPlayer`, `C_PlayerCache.SetEnabled`, `GetPlayerInfoByGUID`, `UnitNameFromGUID` |
|
||||
| [NewItems](docs/API.md#newitems) | `C_NewItems.ClearAll`, `C_NewItems.IsNewItem`, `C_NewItems.RemoveNewItem` |
|
||||
@@ -119,7 +127,8 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
|
||||
| [Profiling](docs/API.md#profiling) | `GetAddOnCPUUsage`, `GetAddOnMemoryUsage`, `GetEventCPUUsage`, `GetFrameCPUUsage`, `GetFunctionCPUUsage`, `GetScriptCPUUsage`, `ResetCPUUsage`, `UpdateAddOnCPUUsage`, `UpdateAddOnMemoryUsage` |
|
||||
| [Quest](docs/API.md#quest) | `C_QuestLog.GetInfo`, `C_QuestLog.GetNumQuestObjectives`, `C_QuestLog.GetQuestDetails`, `C_QuestLog.GetQuestLogTitle`, `C_QuestLog.GetHeaderIndexForQuest`, `C_QuestLog.GetLogIndexForQuestID`, `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogCompletionText`, `GetQuestLogLeaderBoardID` (Turtle WoW only: `C_QuestLog.GetAllCompletedQuestIDs`, `C_QuestLog.IsQuestFlaggedCompleted`, `GetQuestsCompleted`, `QueryQuestsCompleted`) |
|
||||
| [Sound](docs/API.md#sound) | `C_Sound.GetRecentSoundFiles`, `C_Sound.GetSoundScaledVolume`, `C_Sound.IsPlaying`, `C_Sound.PlayItemSound`, `C_Sound.PlaySound`, `C_Sound.PlaySoundWithOptions`, `C_Sound.PlayVocalErrorSound`, `MuteSoundFile`, `PlaySound`, `PlaySoundFile`, `StopSound`, `UnmuteSoundFile` |
|
||||
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.CastAtUnit`, `C_Spell.CastingInfo`, `C_Spell.ChannelInfo`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCastCount`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellDispelType`, `C_Spell.GetSpellEffectInfo`, `C_Spell.GetSpellEffectMechanics`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLevelInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellLossOfControlCooldown`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellRequiredTargetLevel`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsNextMeleeSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellInRange`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.ResetsMeleeSwing`, `C_Spell.SpellHasRange`, `C_Spell.UnitCastingInfo`, `C_Spell.UnitChannelInfo`, `CanDualWield`, `CancelSpellByName`, `CancelUnitBuff`, `CastSpellByID`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetCritChance`, `GetRangedCritChance`, `GetSpellBaseCooldown`, `GetSpellBonusDamage`, `GetSpellBonusHealing`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellRequiredTargetLevel`, `GetSpellSchool`, `IsAttackSpell`, `IsAutoRepeatSpell`, `IsConsumableSpell`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` |
|
||||
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.CastAtUnit`, `C_Spell.CastingInfo`, `C_Spell.ChannelInfo`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCastCount`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellDispelType`, `C_Spell.GetSpellEffectInfo`, `C_Spell.GetSpellEffectMechanics`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLevelInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellLossOfControlCooldown`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellRequiredTargetLevel`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsNextMeleeSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellInRange`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.ResetsMeleeSwing`, `C_Spell.SpellHasRange`, `C_Spell.UnitCastingInfo`, `C_Spell.UnitChannelInfo`, `CanDualWield`, `CancelSpellByName`, `CancelUnitBuff`, `CastSpellByID`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetCritChance`, `GetRangedCritChance`, `GetSpellBaseCooldown`, `GetSpellBonusDamage`, `GetSpellBonusHealing`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellRequiredTargetLevel`, `GetSpellSchool`, `IsAttackSpell`, `IsAutoRepeatSpell`, `IsConsumableSpell`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange`, `UnitCastingDuration`, `UnitChannelDuration` |
|
||||
| [SpellActivationOverlay](docs/API.md#spellactivationoverlay) | `C_SpellActivationOverlay.IsSpellOverlayed`, `IsSpellOverlayed` |
|
||||
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.ContainsAnyDisenchantSpell`, `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetNumSpellBookSkillLines`, `C_SpellBook.GetPlayerSpellsByAura`, `C_SpellBook.GetSkillLineIndexByID`, `C_SpellBook.GetSkillLineName`, `C_SpellBook.GetSkillLineRank`, `C_SpellBook.GetSpellBookItemCastCount`, `C_SpellBook.GetSpellBookItemInfo`, `C_SpellBook.GetSpellBookItemLossOfControlCooldownDuration`, `C_SpellBook.GetSpellBookItemLossOfControlCooldownInfo`, `C_SpellBook.GetSpellBookItemSkillLineIndex`, `C_SpellBook.GetSpellBookSkillLineInfo`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `C_SpellBook.IsAutoAttackSpellBookItem`, `C_SpellBook.IsClassTalentSpellBookItem`, `C_SpellBook.IsRangedAutoAttackSpellBookItem`, `FindSpellBookSlotByID` |
|
||||
| [State](docs/API.md#state) | `CancelShapeshiftForm`, `Dismount`, `GetMirrorTimerInfo`, `GetMirrorTimerProgress`, `GetShapeshiftForm`, `GetShapeshiftFormID`, `GetSheathState`, `IsAssistingRitual`, `IsFalling`, `IsIndoors`, `IsInGroup`, `IsInRaid`, `IsLoggedIn`, `IsLoggingOut`, `IsMounted`, `IsOutdoors`, `IsPlayerInWorld`, `IsStealthed`, `IsSwimming` |
|
||||
| [SwingTimer](docs/API.md#swingtimer) | `C_SwingTimer.EnableRangeCheck`, `C_SwingTimer.IsTargetWithinSwingRange` |
|
||||
@@ -127,14 +136,14 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
|
||||
| [Talent](docs/API.md#talent) | `GetTalentIDByIndex`, `GetTalentSpellID` |
|
||||
| [Targeting](docs/API.md#targeting) | `GetPlayerFacing`, `TargetDirectionEnemy`, `TargetDirectionFriend`, `TargetDirectionFinished`, `TargetNearest`, `TargetNearestEnemyPlayer`, `TargetNearestFriendPlayer`, `InteractUnit`, `C_PlayerInteractionManager.InteractUnit`, `IsTargetLoose` |
|
||||
| [TaxiMap](docs/API.md#taximap) | `C_TaxiMap.GetTaxiNodesForMap`, `C_TaxiMap.GetAllTaxiNodes`, `C_TaxiMap.GetTaxiPaths`, `C_TaxiMap.GetTaxiPathWaypoints`, `C_TaxiMap.GetTaxiRoute` |
|
||||
| [Texture](docs/API.md#texture) | `C_Texture.GetAtlasElementID`, `C_Texture.GetAtlasElements`, `C_Texture.GetAtlasExists`, `C_Texture.GetAtlasID`, `C_Texture.GetAtlasInfo`, `C_Texture.RegisterAtlas` |
|
||||
| [Texture](docs/API.md#texture) | `C_Texture.GetAtlasElementID`, `C_Texture.GetAtlasElements`, `C_Texture.GetAtlasExists`, `C_Texture.GetAtlasID`, `C_Texture.GetAtlasInfo`, `C_Texture.RegisterAtlas`, `SetPortraitTextureFromCreatureDisplayID` |
|
||||
| [Time](docs/API.md#time) | `C_DateAndTime.AdjustTimeByDays`, `C_DateAndTime.AdjustTimeByMinutes`, `C_DateAndTime.CompareCalendarTime`, `C_DateAndTime.GetCalendarTimeFromEpoch`, `C_DateAndTime.GetCurrentCalendarTime`, `C_DateAndTime.GetSecondsUntilDailyReset`, `C_DateAndTime.GetServerTimeLocal`, `C_Timer.After`, `C_Timer.NewTicker`, `C_Timer.NewTimer`, `GetServerTime`, `GetTickTime`, `GetTimeCached` |
|
||||
| [Totem](docs/API.md#totem) | `GetTotemInfo`, `GetTotemTimeLeft`, `GetTotemDuration`, `TargetTotem` |
|
||||
| [Tracking](docs/API.md#tracking) | `GetNumTrackingTypes`, `GetTrackingInfo`, `SetTracking` |
|
||||
| [TradeSkillUI](docs/API.md#tradeskillui) | `C_TradeSkillUI.GetTradeSkillListLink`, `C_TradeSkillUI.GetCraftListLink`, `C_TradeSkillUI.GetTradeSkillListRecipes`, `GetTradeSkillDescription`, `GetTradeSkillRecipeLink`, `IsTradeSkillLinked` |
|
||||
| [TradeSkillUI](docs/API.md#tradeskillui) | `GetTradeSkillListLink`, `GetCraftListLink`, `GetTradeSkillDescription`, `GetTradeSkillRecipeLink`, `IsTradeSkillLinked`, `IsCraftLinked` |
|
||||
| [Trainer](docs/API.md#trainer) | `GetTrainerServiceItemLink` |
|
||||
| [UIColor](docs/API.md#uicolor) | `C_UIColor.GetColors` |
|
||||
| [Unit](docs/API.md#unit) | `CanBeRaidTarget`, `ClosestUnitPosition`, `GetUnitSpeed`, `IsUnitToken`, `SetUnitCursorTexture`, `UnitClassBase`, `UnitCreatedBySpell`, `UnitCreatureFamilyID`, `UnitCreatureID`, `UnitCreatureTypeID`, `UnitDistanceSquared`, `UnitGUID`, `UnitHasIncomingResurrection`, `UnitHealthMissing`, `UnitInLineOfSight`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitOwnerGUID`, `UnitPosition`, `UnitPower`, `UnitPowerMax`, `UnitPowerMissing`, `UnitPowerType`, `UnitRaceBase`, `UnitSelectionColor`, `UnitSelectionType`, `UnitSpellHaste`, `UnitSpellTargetName`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID`, `UnitTokenFromName` |
|
||||
| [Unit](docs/API.md#unit) | `CanBeRaidTarget`, `ClosestUnitPosition`, `GetUnitSpeed`, `IsUnitToken`, `SetUnitCursorTexture`, `UnitClassBase`, `UnitCreatedBySpell`, `UnitCreatureFamilyID`, `UnitCreatureID`, `UnitCreatureTypeID`, `UnitDistanceSquared`, `UnitFacing`, `UnitGUID`, `UnitHasIncomingResurrection`, `UnitHealthMissing`, `UnitInLineOfSight`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitOwnerGUID`, `UnitPosition`, `UnitPower`, `UnitPowerMax`, `UnitPowerMissing`, `UnitPowerType`, `UnitRaceBase`, `UnitSelectionColor`, `UnitSelectionType`, `UnitSpellHaste`, `UnitSpellTargetName`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID`, `UnitTokenFromName` |
|
||||
| [UnitAuras](docs/API.md#unitauras) | `C_UnitAuras.GetAuraDataByAuraInstanceID`, `C_UnitAuras.GetAuraDataByIndex`, `C_UnitAuras.GetAuraDataBySlot`, `C_UnitAuras.GetAuraDataBySpellName`, `C_UnitAuras.GetAuraDispelTypeColor`, `C_UnitAuras.GetAuraSlots`, `C_UnitAuras.GetBuffDataByIndex`, `C_UnitAuras.GetDebuffDataByIndex`, `C_UnitAuras.GetPlayerAuraBySpellID`, `C_UnitAuras.GetUnitAuraBySpellID`, `C_UnitAuras.GetUnitAuras`, `C_UnitAuras.RegisterAuraDurationModifierByTrigger`, `C_UnitAuras.RegisterComboDuration`, `C_UnitAuras.UnitAura`, `C_UnitAuras.UnitAuraBySlot`, `C_UnitAuras.UnitBuff`, `C_UnitAuras.UnitDebuff` |
|
||||
| [VoiceChat](docs/API.md#voicechat) | `C_VoiceChat.GetTtsVoices`, `C_VoiceChat.GetRemoteTtsVoices`, `C_VoiceChat.SpeakText`, `C_VoiceChat.StopSpeakingText`, `C_TTSSettings.GetSpeechRate`, `C_TTSSettings.GetSpeechVolume`, `C_TTSSettings.GetSpeechVoiceID`, `C_TTSSettings.GetVoiceOptionName`, `C_TTSSettings.SetSpeechRate`, `C_TTSSettings.SetSpeechVolume`, `C_TTSSettings.SetVoiceOption`, `C_TTSSettings.SetVoiceOptionByName`, `C_TTSSettings.SetDefaultSettings`, `C_TTSSettings.RefreshVoices` |
|
||||
| [XMLUtil](docs/API.md#xmlutil) | `C_XMLUtil.DoesTemplateExist`, `C_XMLUtil.GetTemplateInfo`, `C_XMLUtil.GetTemplates` |
|
||||
@@ -157,6 +166,7 @@ the glue state because GlueXML had no way to reach them otherwise.
|
||||
| [CharacterList](docs/API.md#characterlist) | `GetSavedCharacterOrder`, `SetSavedCharacterOrder` (persist to `WTF\Account\...\ClassicAPI.txt`) |
|
||||
| CVar | `GetCVar`, `SetCVar`, `RegisterCVar`, `GetCVarDefault`, `C_CVar.GetCVarBool`, `C_CVar.SetTempCVar`, `C_CVar.RemoveTempCVar` (storage is process-global — writes from glue are visible in-world and vice versa) |
|
||||
| [Glue](docs/API.md#glue) | `C_Glue.IsFirstLoadThisSession`, `C_Glue.IsOnGlueScreen` |
|
||||
| [Input](docs/API.md#getcursordelta) | `GetCursorDelta` |
|
||||
| Script | `RunScript` (compile and run a Lua chunk in the glue state's globals — useful for slash-command-style helpers in GlueXML) |
|
||||
| State | `IsLoggedIn` |
|
||||
|
||||
@@ -207,16 +217,25 @@ for what each command does, and
|
||||
|
||||
Each setting is saved in `WTF\Config.wtf`. Read and write them with `GetCVar`
|
||||
/ `SetCVar` or `/console`. Click a group name for the full rules. To change
|
||||
the most common settings and the player cache in the game, open the game
|
||||
menu and click AddOn Options, then ClassicAPI.
|
||||
these settings, the player cache and the ClassicAPI key bindings in the game,
|
||||
open the game menu and click AddOn Options, then ClassicAPI.
|
||||
|
||||
| Group | CVar | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| [Bindings](docs/API.md#actionbuttonusekeydown) | `ActionButtonUseKeyDown` | `0` | `1` uses an action-bar key binding when you press the key. `0` uses it when you release the key. |
|
||||
| [Combat](docs/API.md#stopautoattackontargetchange) | `stopAutoAttackOnTargetChange` | `0` | `1` stops melee auto-attack, Auto Shot and wand shots when you change target. |
|
||||
| [Cast settings](docs/API.md#cast-settings-autostand-autodismount-autounshift) | `autoStand` | `0` | `1` makes you stand up when you cast while you sit. |
|
||||
| | `autoDismount` | `0` | `1` makes you dismount when you cast while you are mounted. |
|
||||
| | `autoUnshift` | `0` | `1` makes you leave your form when a spell cannot be cast in it. |
|
||||
| [Combat log](docs/API.md#kept-entries) | `combatLogRetentionTime` | `300` | The number of seconds that the combat log keeps an entry. |
|
||||
| [Stand, dismount and form](docs/API.md#stand-dismount-and-form-settings-autostand-autodismount-autounshift) | `autoStand` | `0` | `1` makes you stand up when you cast, loot or open the flight map while you sit. |
|
||||
| | `autoDismount` | `0` | `1` makes you dismount when you cast, use an object, loot, attack or open the flight map while you are mounted. |
|
||||
| | `autoUnshift` | `0` | `1` makes you leave your form when you cast a spell that cannot be cast in it, use an item, or open the flight map. It also lets you talk to NPCs and use objects in druid forms and Ghost Wolf. |
|
||||
| [Bag sorting](docs/API.md#c_containergetsortbagsrighttoleft--c_containersetsortbagsrighttoleftenable) | `sortBagsRightToLeft` | `0` | `1` makes a sort fill your bags from the last slot of the last bag. |
|
||||
| | `backpackAutosortDisabled` | `0` | `1` keeps the items in your backpack in place when you sort your bags. |
|
||||
| | `bankAutosortDisabled` | `0` | `1` keeps the items in the main bank slots in place when you sort your bank. |
|
||||
| [Expanded cursors](docs/API.md#expanded-cursors-expandedcursors) | `expandedCursors` | `0` | `1` makes the cursor show what you can do with an NPC or an object: an anvil for repairs, a hearthstone for an inn, a horseshoe for a stable, `!` or `?` for quests, and a fish hook for your fishing bobber. |
|
||||
| [Equipment sets](docs/API.md#equipment-sets-on-item-tooltips) | `dontShowEquipmentSetsOnItems` | `0` | `1` removes the Equipment Sets line from item tooltips. |
|
||||
| [Name completion](docs/API.md#name-completion-settings) | `autoCompleteResortNamesOnRecency` | `1` | `1` puts the players that you whispered first in `C_AutoComplete` results, the most recent first. `0` uses alphabetical order. |
|
||||
| | `autoCompleteWhenEditingFromCenter` | `1` | `1` also matches the text after the cursor when you type inside a name. |
|
||||
| | `autoCompleteUseContext` | `1` | `0` makes `C_AutoComplete` ignore `includeFlags` and `excludeFlags`. |
|
||||
| [Profiling](docs/API.md#scriptprofile) | `scriptProfile` | `0` | `1` turns on CPU profiling for the `*CPUUsage` functions. The change starts after `/reload`. |
|
||||
| [Soft interact](docs/API.md#soft-interact-softinteract--anyinteract) | `SoftTargetInteract` | `3` | Turns `softinteract` on: `2` keyboard and mouse, `3` always, `0` / `1` off. |
|
||||
| | `SoftTargetInteractArc` | `0` | Where the object can be: `0` straight ahead of the camera, `1` within 30° of it, `2` any direction. |
|
||||
@@ -233,7 +252,9 @@ menu and click AddOn Options, then ClassicAPI.
|
||||
| | `SoftTargetIconEnemy` / `SoftTargetIconFriend` | `0` | `1` shows an icon over `softenemy` / `softfriend`. |
|
||||
| | `SoftTargetLowPriorityIcons` | `0` | `1` also shows the normal pointer as an icon. |
|
||||
| | `SoftTargetNameplateSize` | `19` | Size of all soft-target icons. |
|
||||
| [Localization](docs/API.md#breakuplargenumbers-cvar) | `breakUpLargeNumbers` | `1` | `1` puts a separator between each group of three digits in `BreakUpLargeNumbers`. `0` writes no separator. |
|
||||
| [Cooldown](docs/API.md#createframecooldown--name-parent-template) | `countdownForCooldowns` | `0` | `1` shows the time left as numbers on cooldown frames. |
|
||||
| `ScriptAnimationUtil` | `ShakeStrengthUI` | `1` | Scales how far `ScriptAnimationUtil.ShakeFrame` moves a frame. `0` turns the shake off. |
|
||||
| [NamePlate](docs/API.md#nameplate) | `nameplateNotSelectedAlpha` | `0.5` | Alpha of a non-target default nameplate, `0` to `1`. |
|
||||
| | `nameplateSelectedAlpha` | `1` | Alpha of the target's default nameplate, `0` to `1`. |
|
||||
| [Text to speech](docs/API.md#tts-cvars) | `ttsVoice` | `0` | The voice that `C_VoiceChat.SpeakText` uses. |
|
||||
@@ -266,6 +287,8 @@ when launching with `-console`), not as Lua functions. See the
|
||||
|
||||
| Event | Payload |
|
||||
|-------|---------|
|
||||
| `ACTION_RANGE_CHECK_UPDATE` | `slot, isInRange, checksRange` (slots with `C_ActionBar.EnableActionRangeCheck` on) |
|
||||
| `ACTION_USABLE_CHANGED` | `changes` (a `{ slot, usable, noMana }` table for each slot that changed) |
|
||||
| `BAG_NEW_ITEMS_UPDATED` | *(none)* |
|
||||
| `BAG_UPDATE_DELAYED` | *(none)* |
|
||||
| `CHAT_MSG_*` | adds `lineID, guid` as `arg11, arg12` after the existing ten ([details](docs/API.md#chat_msg_-arguments-lineid-arg11-and-guid-arg12)) |
|
||||
@@ -297,6 +320,7 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `PLAYER_AVG_ITEM_LEVEL_READY` | *(none)* |
|
||||
| `PLAYER_EQUIPMENT_CHANGED` | `equipmentSlot, hasCurrent` |
|
||||
| `PLAYER_FOCUS_CHANGED` | *(none)* |
|
||||
| `PLAYER_MOUNT_DISPLAY_CHANGED` | *(none)* |
|
||||
| `PLAYER_SOFT_ENEMY_CHANGED` | *(none)* |
|
||||
| `PLAYER_SOFT_FRIEND_CHANGED` | *(none)* |
|
||||
| `PLAYER_SOFT_INTERACT_CHANGED` | `oldTarget, newTarget` |
|
||||
@@ -316,7 +340,16 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `QUEST_TURNED_IN` | `questID, xpReward, moneyReward` |
|
||||
| `SELF_RES_SPELL_CHANGED` | *(none)* |
|
||||
| `SOUNDKIT_FINISHED` | `soundHandle` (only for sounds played with `runFinishCallback`) |
|
||||
| `SPELL_ACTIVATION_OVERLAY_SHOW` | `spellID, texture, locationType, scale, r, g, b` ([details](docs/API.md#spell_activation_overlay_show--spell_activation_overlay_hide-events)) |
|
||||
| `SPELL_ACTIVATION_OVERLAY_HIDE` | `spellID` (`nil` hides every alert) |
|
||||
| `SPELL_ACTIVATION_OVERLAY_GLOW_SHOW` | `spellID` ([details](docs/API.md#spell_activation_overlay_glow_show--spell_activation_overlay_glow_hide-events)) |
|
||||
| `SPELL_ACTIVATION_OVERLAY_GLOW_HIDE` | `spellID` |
|
||||
| `UI_SCALE_CHANGED` | *(none)* |
|
||||
| `UNIT_AREA_CHANGED` | `unitTarget` |
|
||||
| `UNIT_AURA_DELTA` | `unitTarget, updateInfo` (the instance IDs of the auras added, updated, and removed) |
|
||||
| `UNIT_CONNECTION` | `unitTarget, isConnected` |
|
||||
| `UNIT_DISTANCE_CHECK_UPDATE` | `unitTarget, isInDistance` |
|
||||
| `UNIT_IN_RANGE_UPDATE` | `unitTarget, isInRange` |
|
||||
| `UNIT_SPELLCAST_SENT` | `"player", target, castGUID, spellID, spellName, rank` |
|
||||
| `UNIT_SPELLCAST_START` | `unit, castGUID, spellID, spellName, rank` |
|
||||
| `UNIT_SPELLCAST_STOP` | `unit, castGUID, spellID, spellName, rank` |
|
||||
@@ -332,6 +365,12 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `UNIT_SPELLCAST_RETICLE_CLEAR` | `"player", "", spellID, spellName, rank` |
|
||||
| `UNIT_SPELLCAST_INTERRUPTIBLE` | `unit` |
|
||||
| `UNIT_SPELLCAST_NOT_INTERRUPTIBLE` | `unit` |
|
||||
| `UNIT_MAXPOWER` | `unitTarget, powerType` |
|
||||
| `UNIT_POWER_FREQUENT` | `unitTarget, powerType` |
|
||||
| `UNIT_POWER_UPDATE` | `unitTarget, powerType` |
|
||||
| `UNIT_SPELL_HASTE` | `unitTarget` |
|
||||
| `UNIT_TARGET` | `unitTarget` |
|
||||
| `UNIT_TARGETABLE_CHANGED` | `unitTarget` |
|
||||
| `UPDATE_INVENTORY_DURABILITY` | *(none)* |
|
||||
| `UPDATE_SHAPESHIFT_FORM` | *(none)* |
|
||||
| `USER_WAYPOINT_UPDATED` | *(none)* |
|
||||
@@ -362,7 +401,11 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| Spellbook bank | `Enum.SpellBookSpellBank.{Player,Pet}` (0–1) |
|
||||
| Spellbook item type | `Enum.SpellBookItemType.{None,Spell,FutureSpell,PetAction,Flyout}` (0–4; 1.12 only yields `Spell`/`PetAction`) |
|
||||
| Player swing type | `Enum.PlayerSwingType.{MainHand,OffHand,Ranged}` (0–2) |
|
||||
| Screen location type | `Enum.ScreenLocationType.{Center,Left,Right,Top,Bottom,TopLeft,TopRight,LeftOutside,RightOutside,LeftRight,TopBottom,LeftRightOutside}` (0–11) |
|
||||
| Self-resurrect option type | `Enum.SelfResurrectOptionType.{Spell,Item}` (0–1) |
|
||||
| Autocomplete entry flag | `Enum.AutoCompleteEntryFlag.{InGroup,InGuild,Friend,Bnet,InteractedWith,Online,InAOI,AccountCharacter,RecentPlayer}` (bits 1–256) |
|
||||
| Model blend operation | `Enum.ModelBlendOperation.{None,Anim}` (0–1) |
|
||||
| Autocomplete priority | `Enum.AutoCompletePriority.{Other,Interacted,InGroup,Guild,Friend,AccountCharacter,AccountCharacterSameRealm}` (0–6) |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -418,10 +461,10 @@ Download the prebuilt `ClassicAPI.dll` from the
|
||||
3. Add `ClassicAPI.dll` to `dlls.txt`.
|
||||
4. Launch the game with `VanillaFixes.exe`.
|
||||
|
||||
The `!!!ClassicAPI` and `ClassicAPI_DebugTools` addons are *inside* the
|
||||
DLL. You do not download or install them. The `src/embed/AddOns/` folder
|
||||
in this repository is source code for the DLL. Do not copy it into your
|
||||
game.
|
||||
The `!!!ClassicAPI`, `ClassicAPI_DebugTools` and `ClassicAPI_EventTrace`
|
||||
addons are *inside* the DLL. You do not download or install them. The
|
||||
`src/embed/AddOns/` folder in this repository is source code for the DLL.
|
||||
Do not copy it into your game.
|
||||
|
||||
## Bundled addon: !!!ClassicAPI
|
||||
|
||||
@@ -432,8 +475,9 @@ functions but that consumer code still expects to find as globals —
|
||||
`CallbackRegistryMixin`, `EventRegistry`, `ColorMixin` + `CreateColor`,
|
||||
`Item` / `ItemLocation`, `MathUtil`(`Lerp` / `Clamp` / `CreateCounter`),
|
||||
`TableUtil` (`tCompare`, `MergeTable`, `SafePack`, etc.), `EventUtil`
|
||||
(`ContinueOnAddOnLoaded` and more), `DataProvider`, `CircularBuffer`,
|
||||
`EasingUtil`, `Interpolator`, `EnumUtil`, `StringUtil`, `AnchorUtil`,
|
||||
(`ContinueOnAddOnLoaded` and more), `FormattingUtil` (`GetMoneyString`,
|
||||
`FormatLargeNumber` and more), `FrameUtil`, `DataProvider`, `CircularBuffer`,
|
||||
`EasingUtil`, `ScriptAnimationUtil`, `Interpolator`, `EnumUtil`, `StringUtil`, `AnchorUtil`,
|
||||
`GridLayoutUtil`, and the layout frame templates (`VerticalLayoutFrame`,
|
||||
`HorizontalLayoutFrame`, `ResizeLayoutFrame`, `GridLayoutFrame`,
|
||||
`StaticGridLayoutFrame`). It also holds the conditional slash
|
||||
@@ -465,22 +509,40 @@ without a DLL rebuild. Without this file, the game uses the copy with the
|
||||
newer `## Version:`. The copy is hidden and always enabled, the same as
|
||||
the embedded addon.
|
||||
|
||||
## Lua errors
|
||||
|
||||
ClassicAPI replaces the game's Lua error box with a Lua error window. The
|
||||
window opens when an error occurs. It shows the message, the time, the count,
|
||||
the stack and the local variables of each error. You can select and copy the
|
||||
text. The arrow buttons show the previous and the next error. The
|
||||
**Reload UI** button reloads the interface.
|
||||
|
||||
If you use `!BugGrabber` (with BugSack), BugGrabber gets the errors and the
|
||||
window does not open. If an addon sets its own error handler, the window also
|
||||
does not open.
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `/luaerrors` (or `/scripterrors`) | Opens or closes the Lua error window. |
|
||||
| `/luaerrors clear` | Removes all errors from the window. |
|
||||
|
||||
## Debug tools
|
||||
|
||||
ClassicAPI contains a backport of Blizzard's `Blizzard_DebugTools` addon,
|
||||
with the name `ClassicAPI_DebugTools`. The DLL contains this addon, so you do
|
||||
not install it. The addon is not in the AddOns list, and you cannot disable it.
|
||||
ClassicAPI contains backports of two Blizzard addons: `Blizzard_DebugTools`,
|
||||
with the name `ClassicAPI_DebugTools`, and `Blizzard_EventTrace`, with the
|
||||
name `ClassicAPI_EventTrace`. The DLL contains these addons, so you do not
|
||||
install them. They are not in the AddOns list, and you cannot disable them.
|
||||
|
||||
The slash commands are always available. The tools load the first time
|
||||
that you use one of these commands:
|
||||
The slash commands are always available. Each addon loads the first time
|
||||
that you use one of its commands:
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `/dump <expr>` | Shows a Lua value in the chat frame. It shows tables and all return values. Use it to examine the return values of `GetSpellInfo`, `C_Item.GetItemInfoInstant`, and other functions. |
|
||||
| `/etrace` | Opens the event trace window. Other forms: `/etrace start`, `/etrace stop`, `/etrace add EVENT`, and more. |
|
||||
| `/framestack [showHidden [showRegions]]` (or `/fstack`) | Shows a tooltip with a list of the frames under the cursor. A `-->` mark shows one frame, with its anchors and the file and line that made it. Left Alt and Right Alt move the mark. Ctrl opens the marked frame in the table inspector. Shift shows its texture files. |
|
||||
| `/etrace` (or `/eventtrace`) | Opens or closes the event trace window. The window logs each event with its time and its arguments. Move the mouse over an event to see its arguments, and the contents of each table argument. You can search the log, hide events with the filter, and pause the log. `/etrace mark <text>` adds a line of text to the log. |
|
||||
| `/framestack [showHidden [showRegions]]` (or `/fstack`) | Shows a tooltip with a list of the frames under the cursor. A `-->` mark shows one frame, with its anchors and the file and line that made it. Left Alt and Right Alt move the mark. Ctrl opens the marked frame in the table inspector. Shift shows its texture files. The tooltip also shows the addon that made the marked frame. When `scriptProfile` is `1`, it shows the CPU time of the frame scripts. |
|
||||
| `/tinspect [expr]` (or `/tins`) | Opens a table inspector on the result of `expr`. With no table, it opens on the frame under the cursor. The window shows the anchors and the fields of the table, grouped by type. You can edit numbers, strings and booleans, filter the lines, and open a child table or frame. |
|
||||
| `/luaerrors` (or `/scripterrors`) | Opens the Lua error window. |
|
||||
| `/profiler` (or `/prof`) | Opens the profiler. The profiler shows the Lua memory, the allocations and the CPU time of each addon, frame, event and global function. `/profiler widget` shows a small window with the Lua memory, the frame rate and the addons that use the most. `/profiler reset` sets the counters to zero. CPU times need `scriptProfile` `1`. `/profiler profile on` sets it and reloads the UI. |
|
||||
|
||||
## Building
|
||||
|
||||
|
||||
@@ -149,7 +149,7 @@ on a frame timer" goal landed as the request/event pair below:
|
||||
cache (or synchronously if it was already loaded).
|
||||
|
||||
Together those replace the polyfill's 180-second polling frame
|
||||
(`ItemEventListener` in `Util/ItemUtil.lua:254-372`) — addons can now
|
||||
(`ItemEventListener`, now in `Util/AsyncCallbackSystem.lua`) — addons can now
|
||||
fire `RequestLoadItemDataByID(id)` and listen for
|
||||
`ITEM_DATA_LOAD_RESULT` instead.
|
||||
|
||||
|
||||
+111
-54
@@ -1,10 +1,10 @@
|
||||
# embed_resources.cmake — embed the addons and the bindings XML as Win32
|
||||
# RCDATA resources.
|
||||
# embed_resources.cmake — embed the addons, the bindings XML and the
|
||||
# built-in textures as Win32 RCDATA resources.
|
||||
#
|
||||
# Runs at configure time. It writes two files into OUT_DIR:
|
||||
#
|
||||
# embedded.rc one `<id> RCDATA "<path>"` line per file; rc.exe
|
||||
# reads each file's gzipped copy from disk.
|
||||
# embedded.rc one `<id> RCDATA "<path>"` line per pack; rc.exe
|
||||
# reads each gzipped pack from disk.
|
||||
# embedded_resources.h the IDs the DLL looks up at runtime through
|
||||
# src/embed/Resource.h:
|
||||
#
|
||||
@@ -12,14 +12,31 @@
|
||||
# namespace Addons::Embedded::Manifest {
|
||||
# constexpr const char *kAddons[] = { ... }; // in ADDONS order
|
||||
# constexpr size_t kAddonCount = ...;
|
||||
# struct File { const char *path; int resourceId; };
|
||||
# struct File { const char *path; int resourceId; int index; };
|
||||
# constexpr File kFiles[] = { ... };
|
||||
# constexpr size_t kFileCount = ...;
|
||||
# }
|
||||
# namespace Texture::Builtin::Manifest {
|
||||
# struct File { const char *path; int resourceId; int index; };
|
||||
# constexpr File kFiles[] = { ... };
|
||||
# constexpr size_t kFileCount = ...;
|
||||
# }
|
||||
#
|
||||
# File paths are relative to ADDONS_ROOT, in backslash form
|
||||
# Each resource is a pack: one gzip stream of several files, which
|
||||
# pack_files.cmake joins at build time into `OUT_DIR/embed/<name>.pack.gz`.
|
||||
# Resource 1 is the bindings XML, a pack of one; then each addon is one
|
||||
# pack, in ADDONS order; then the textures, one pack. A pack compresses each
|
||||
# file against the files before it, so the addon text packs to about 22% of
|
||||
# its size, against 27% for one gzip stream per file. The DLL inflates a
|
||||
# whole pack with the engine's zlib on first use (src/embed/Resource.cpp),
|
||||
# so the load-on-demand addon costs nothing until it loads.
|
||||
#
|
||||
# Addon file paths are relative to ADDONS_ROOT, in backslash form
|
||||
# (`!!!ClassicAPI\Util\Print.lua`) — the suffix the engine builds after
|
||||
# `Interface\AddOns\`. The DLL compares them ignoring case.
|
||||
# `Interface\AddOns\`. Texture paths are relative to TEXTURES_ROOT, where
|
||||
# each file sits at its game path (`Interface\Cursor\Quest.blp`), for
|
||||
# src/texture/Builtin.cpp. The DLL compares both ignoring case. A file's
|
||||
# index is its place in its pack, which is in path order.
|
||||
#
|
||||
# Each addon's `.toc` is the one file that is not embedded verbatim: its
|
||||
# `## Version:` line is stamped with ADDON_VERSION into a copy under
|
||||
@@ -28,30 +45,38 @@
|
||||
# never embedded: it must reflect on-disk presence only (see
|
||||
# DiskHasDevMarker in src/addons/Embedded.cpp).
|
||||
#
|
||||
# Every resource is gzipped at build time (gzip_file.cmake) into
|
||||
# `OUT_DIR/embed/<path>.gz`, and the DLL inflates it with the engine's zlib on
|
||||
# first use (src/embed/Resource.cpp). The addon text compresses to about a
|
||||
# quarter of its size.
|
||||
#
|
||||
# Rebuild triggers: adding or removing a file reconfigures through the
|
||||
# CONFIGURE_DEPENDS glob; editing a toc reconfigures through
|
||||
# CMAKE_CONFIGURE_DEPENDS; editing any other file re-gzips it through its
|
||||
# custom command, which recompiles the .rc through its OBJECT_DEPENDS.
|
||||
# CMAKE_CONFIGURE_DEPENDS; editing any other file re-packs its addon through
|
||||
# the pack's custom command, which recompiles the .rc through its
|
||||
# OBJECT_DEPENDS.
|
||||
#
|
||||
# Usage:
|
||||
# embed_resources(TARGET <t> ADDONS_ROOT <dir> ADDONS <folder names...>
|
||||
# ADDON_VERSION <v> BINDINGS_XML <file> OUT_DIR <dir>)
|
||||
# ADDON_VERSION <v> BINDINGS_XML <file>
|
||||
# TEXTURES_ROOT <dir> OUT_DIR <dir>)
|
||||
|
||||
set(_EMBED_GZIP_SCRIPT "${CMAKE_CURRENT_LIST_DIR}/gzip_file.cmake")
|
||||
set(_EMBED_PACK_SCRIPT "${CMAKE_CURRENT_LIST_DIR}/pack_files.cmake")
|
||||
|
||||
# Add the build-time command that gzips `src` into `gz`.
|
||||
function(_embed_gzip src gz)
|
||||
# Add the build-time command that packs `ARGN` (absolute paths, in pack
|
||||
# order) into `<out_dir>/embed/<name>.pack.gz`, and set `gz` in the caller
|
||||
# to that path. The paths reach the script through a generated list file:
|
||||
# an addon's paths are too long for one command line.
|
||||
function(_embed_pack out_dir name)
|
||||
set(pack "${out_dir}/embed/${name}.pack")
|
||||
set(list "set(FILES\n")
|
||||
foreach(path ${ARGN})
|
||||
string(APPEND list " [==[${path}]==]\n")
|
||||
endforeach()
|
||||
string(APPEND list ")\n")
|
||||
_embed_write_if_changed("${pack}.cmake" "${list}")
|
||||
add_custom_command(
|
||||
OUTPUT "${gz}"
|
||||
COMMAND "${CMAKE_COMMAND}" "-DIN=${src}" "-DOUT=${gz}"
|
||||
-P "${_EMBED_GZIP_SCRIPT}"
|
||||
DEPENDS "${src}" "${_EMBED_GZIP_SCRIPT}"
|
||||
OUTPUT "${pack}.gz"
|
||||
COMMAND "${CMAKE_COMMAND}" "-DLIST=${pack}.cmake" "-DOUT=${pack}.gz"
|
||||
-P "${_EMBED_PACK_SCRIPT}"
|
||||
DEPENDS ${ARGN} "${pack}.cmake" "${_EMBED_PACK_SCRIPT}"
|
||||
VERBATIM)
|
||||
set(gz "${pack}.gz" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
# Write `content` to `path` only when it differs, so an unchanged
|
||||
@@ -64,63 +89,79 @@ function(_embed_write_if_changed path content)
|
||||
endfunction()
|
||||
|
||||
function(embed_resources)
|
||||
cmake_parse_arguments(E "" "TARGET;ADDONS_ROOT;ADDON_VERSION;BINDINGS_XML;OUT_DIR"
|
||||
cmake_parse_arguments(E ""
|
||||
"TARGET;ADDONS_ROOT;ADDON_VERSION;BINDINGS_XML;TEXTURES_ROOT;OUT_DIR"
|
||||
"ADDONS" ${ARGN})
|
||||
|
||||
set(rc "// Generated by cmake/embed_resources.cmake - do not edit.\n\n")
|
||||
set(addon_names "")
|
||||
set(manifest "")
|
||||
set(inputs "")
|
||||
set(id 1)
|
||||
|
||||
set(gz "${E_OUT_DIR}/embed/Bindings.xml.gz")
|
||||
_embed_gzip("${E_BINDINGS_XML}" "${gz}")
|
||||
string(APPEND rc "${id} RCDATA \"${gz}\"\n")
|
||||
set(bindings_id 1)
|
||||
_embed_pack("${E_OUT_DIR}" "Bindings.xml" "${E_BINDINGS_XML}")
|
||||
string(APPEND rc "${bindings_id} RCDATA \"${gz}\"\n")
|
||||
list(APPEND inputs "${gz}")
|
||||
set(bindings_id ${id})
|
||||
math(EXPR id "${id} + 1")
|
||||
|
||||
set(files "")
|
||||
set(id 2)
|
||||
foreach(addon ${E_ADDONS})
|
||||
string(APPEND addon_names " \"${addon}\",\n")
|
||||
file(GLOB_RECURSE addon_files CONFIGURE_DEPENDS RELATIVE
|
||||
file(GLOB_RECURSE files CONFIGURE_DEPENDS RELATIVE
|
||||
"${E_ADDONS_ROOT}" "${E_ADDONS_ROOT}/${addon}/*")
|
||||
list(FILTER addon_files EXCLUDE REGEX "(^|/)\\.classicapi-dev$")
|
||||
list(APPEND files ${addon_files})
|
||||
endforeach()
|
||||
list(SORT files) # deterministic IDs
|
||||
foreach(rel ${files})
|
||||
set(abs "${E_ADDONS_ROOT}/${rel}")
|
||||
if(rel MATCHES "\\.toc$")
|
||||
file(READ "${abs}" toc)
|
||||
string(REGEX REPLACE "## Version:[^\n\r]*"
|
||||
"## Version: ${E_ADDON_VERSION}" toc "${toc}")
|
||||
set(stamped "${E_OUT_DIR}/embed/${rel}")
|
||||
_embed_write_if_changed("${stamped}" "${toc}")
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${abs}")
|
||||
set(abs "${stamped}")
|
||||
endif()
|
||||
set(gz "${E_OUT_DIR}/embed/${rel}.gz")
|
||||
_embed_gzip("${abs}" "${gz}")
|
||||
list(FILTER files EXCLUDE REGEX "(^|/)\\.classicapi-dev$")
|
||||
list(SORT files) # deterministic indexes
|
||||
set(paths "")
|
||||
set(index 0)
|
||||
foreach(rel ${files})
|
||||
set(abs "${E_ADDONS_ROOT}/${rel}")
|
||||
if(rel MATCHES "\\.toc$")
|
||||
file(READ "${abs}" toc)
|
||||
string(REGEX REPLACE "## Version:[^\n\r]*"
|
||||
"## Version: ${E_ADDON_VERSION}" toc "${toc}")
|
||||
set(stamped "${E_OUT_DIR}/embed/${rel}")
|
||||
_embed_write_if_changed("${stamped}" "${toc}")
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${abs}")
|
||||
set(abs "${stamped}")
|
||||
endif()
|
||||
list(APPEND paths "${abs}")
|
||||
string(REPLACE "/" "\\\\" rel_bs "${rel}")
|
||||
string(APPEND manifest " { \"${rel_bs}\", ${id}, ${index} },\n")
|
||||
math(EXPR index "${index} + 1")
|
||||
endforeach()
|
||||
_embed_pack("${E_OUT_DIR}" "${addon}" ${paths})
|
||||
string(APPEND rc "${id} RCDATA \"${gz}\"\n")
|
||||
list(APPEND inputs "${gz}")
|
||||
string(REPLACE "/" "\\\\" rel_bs "${rel}")
|
||||
string(APPEND manifest " { \"${rel_bs}\", ${id} },\n")
|
||||
math(EXPR id "${id} + 1")
|
||||
endforeach()
|
||||
|
||||
file(GLOB_RECURSE textures CONFIGURE_DEPENDS RELATIVE
|
||||
"${E_TEXTURES_ROOT}" "${E_TEXTURES_ROOT}/*")
|
||||
list(SORT textures) # deterministic indexes
|
||||
set(texture_paths "")
|
||||
set(texture_manifest "")
|
||||
set(index 0)
|
||||
foreach(rel ${textures})
|
||||
list(APPEND texture_paths "${E_TEXTURES_ROOT}/${rel}")
|
||||
string(REPLACE "/" "\\\\" rel_bs "${rel}")
|
||||
string(APPEND texture_manifest " { \"${rel_bs}\", ${id}, ${index} },\n")
|
||||
math(EXPR index "${index} + 1")
|
||||
endforeach()
|
||||
_embed_pack("${E_OUT_DIR}" "Textures" ${texture_paths})
|
||||
string(APPEND rc "${id} RCDATA \"${gz}\"\n")
|
||||
list(APPEND inputs "${gz}")
|
||||
|
||||
_embed_write_if_changed("${E_OUT_DIR}/embedded.rc" "${rc}")
|
||||
_embed_write_if_changed("${E_OUT_DIR}/embedded_resources.h"
|
||||
"// Generated by cmake/embed_resources.cmake — do not edit.
|
||||
// Resource IDs for the RCDATA entries in embedded.rc. Load them through
|
||||
// src/embed/Resource.h.
|
||||
// Resource IDs for the RCDATA packs in embedded.rc, and each embedded file's
|
||||
// place in them. Load them through src/embed/Resource.h.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace Embed::Ids {
|
||||
constexpr int kBindingsXml = ${bindings_id};
|
||||
constexpr int kBindingsXml = ${bindings_id}; // a pack of the one file
|
||||
} // namespace Embed::Ids
|
||||
|
||||
namespace Addons::Embedded::Manifest {
|
||||
@@ -133,7 +174,8 @@ constexpr size_t kAddonCount = sizeof(kAddons) / sizeof(kAddons[0]);
|
||||
|
||||
struct File {
|
||||
const char *path;
|
||||
int resourceId;
|
||||
int resourceId; // its addon's pack
|
||||
int index; // its place in the pack
|
||||
};
|
||||
|
||||
constexpr File kFiles[] = {
|
||||
@@ -142,6 +184,21 @@ ${manifest}};
|
||||
constexpr size_t kFileCount = sizeof(kFiles) / sizeof(kFiles[0]);
|
||||
|
||||
} // namespace Addons::Embedded::Manifest
|
||||
|
||||
namespace Texture::Builtin::Manifest {
|
||||
|
||||
struct File {
|
||||
const char *path; // the game path
|
||||
int resourceId; // the textures' pack
|
||||
int index; // its place in the pack
|
||||
};
|
||||
|
||||
constexpr File kFiles[] = {
|
||||
${texture_manifest}};
|
||||
|
||||
constexpr size_t kFileCount = sizeof(kFiles) / sizeof(kFiles[0]);
|
||||
|
||||
} // namespace Texture::Builtin::Manifest
|
||||
")
|
||||
|
||||
add_custom_target(${E_TARGET}_embed DEPENDS ${inputs})
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
# gzip_file.cmake — gzip one file at build time, for embed_resources.cmake.
|
||||
#
|
||||
# cmake -DIN=<file> -DOUT=<file.gz> -P gzip_file.cmake
|
||||
#
|
||||
# The raw format holds the one file's bytes with no archive entry around
|
||||
# them, so OUT is a plain gzip stream of IN.
|
||||
file(ARCHIVE_CREATE OUTPUT "${OUT}" PATHS "${IN}" FORMAT raw
|
||||
COMPRESSION GZip COMPRESSION_LEVEL 9)
|
||||
@@ -0,0 +1,50 @@
|
||||
# pack_files.cmake — join files into one gzipped pack at build time, for
|
||||
# embed_resources.cmake.
|
||||
#
|
||||
# cmake -DLIST=<list.cmake> -DOUT=<pack.gz> -P pack_files.cmake
|
||||
#
|
||||
# LIST sets FILES to the paths to join, in order. The pack is one gzip
|
||||
# stream, so each file compresses against the files before it: the addon
|
||||
# text packs about 16% smaller than when each file is gzipped alone. Inside
|
||||
# the stream is an index, each file's size as a decimal line and then an
|
||||
# empty line, followed by the files' bytes in FILES order. The DLL splits
|
||||
# it in src/embed/Resource.cpp.
|
||||
|
||||
include("${LIST}")
|
||||
|
||||
set(index "")
|
||||
foreach(file IN LISTS FILES)
|
||||
file(SIZE "${file}" size)
|
||||
string(APPEND index "${size}\n")
|
||||
endforeach()
|
||||
# Not file(WRITE): on Windows it writes each \n as \r\n.
|
||||
file(CONFIGURE OUTPUT "${OUT}.index" CONTENT "${index}\n" @ONLY NEWLINE_STYLE UNIX)
|
||||
|
||||
# Join `ARGN` byte for byte into `out`.
|
||||
function(cat out)
|
||||
execute_process(COMMAND "${CMAKE_COMMAND}" -E cat ${ARGN}
|
||||
OUTPUT_FILE "${out}" RESULT_VARIABLE rc)
|
||||
if(NOT rc EQUAL 0)
|
||||
message(FATAL_ERROR "pack_files.cmake: cannot join into ${out}: ${rc}")
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
# `cmake -E cat` takes its files on the command line, which Windows limits
|
||||
# to 32767 characters: join in batches of 100, then join the batches.
|
||||
set(parts "${OUT}.index")
|
||||
list(LENGTH FILES count)
|
||||
set(first 0)
|
||||
while(first LESS count)
|
||||
list(SUBLIST FILES ${first} 100 batch)
|
||||
set(part "${OUT}.part${first}")
|
||||
cat("${part}" ${batch})
|
||||
list(APPEND parts "${part}")
|
||||
math(EXPR first "${first} + 100")
|
||||
endwhile()
|
||||
cat("${OUT}.raw" ${parts})
|
||||
|
||||
# The raw format holds the one file's bytes with no archive entry around
|
||||
# them, so OUT is a plain gzip stream.
|
||||
file(ARCHIVE_CREATE OUTPUT "${OUT}" PATHS "${OUT}.raw" FORMAT raw
|
||||
COMPRESSION GZip COMPRESSION_LEVEL 9)
|
||||
file(REMOVE ${parts} "${OUT}.raw")
|
||||
+5988
-1228
File diff suppressed because it is too large
Load Diff
@@ -27,24 +27,22 @@ The DLL hooks the engine's own text pipeline — no companion addon:
|
||||
pen is lazy and omits the last glyph's own advance, so a coin sat on its
|
||||
digits; the emitter now adds the true terminal advance from the engine's own
|
||||
glyph-advance helpers (`550cf5e`).
|
||||
- **Rendering** — the paint-pass co-hook `FUN_005c8fe0` walks the icon records
|
||||
and queues each to `Text::InlineTexturePool`, which places the region on the
|
||||
next frame tick (never mid-render).
|
||||
- **Chat has no one-frame lag** — a ScrollingMessageFrame re-`SetText`s every
|
||||
visible line on each new message, and the engine builds each line lazily at
|
||||
paint, so a line's icons would land one frame after its glyphs (glyphs draw
|
||||
synchronously; the icon regions are separate objects whose placement is only
|
||||
known once the line builds mid-paint, after the frame's texture pass already
|
||||
drew). A co-hook on the SMF display refresh (`FUN_00788750`, which runs
|
||||
pre-render from the frame's own methods) forces each icon-bearing line to
|
||||
resolve+build and applies its icon placement synchronously — before the frame
|
||||
draws — so icons move in lockstep with their text. It reuses the engine's own
|
||||
`Realize(0)` → `RebuildString` → ensure-built path and the same placement math
|
||||
the paint flush uses (`ComputePlacementsForNode`), so the later flush dedups to
|
||||
a no-op. Additive and best-effort: any line whose rect isn't resolvable in time
|
||||
falls back to the paint + tick pipeline (correct, one-frame lagged). A pure
|
||||
scroll or drag never lagged — the icons are anchored to the line and the engine
|
||||
moves them for free; only content changes on a reused line needed this.
|
||||
- **Rendering** — 4.3.4's `CSimpleEmbeddedTexture` model. Each icon is a
|
||||
pooled `CSimpleTexture` with no frame, anchored to its fontstring: it is in
|
||||
no draw layer, so the fontstring draws it. The fontstring's batch call
|
||||
(`FUN_FONTSTRING_EMIT` `0x00772E50`, 4.3.4's `CSimpleFontString::Draw` →
|
||||
`QueueTextures`) adds the icons right after its text, and only when the text
|
||||
went in, so an icon shows exactly when its line does — hidden, faded out, off
|
||||
screen, or scrolled off the top of a chat frame — in the same frame.
|
||||
- **No one-frame lag** — 4.3.4 sets the embedded textures up in `UpdateString`,
|
||||
right after it builds the string. The `RebuildString` co-hook does the same:
|
||||
a rebuild drops the fontstring's icons, lays the new node out now (the
|
||||
ensure-built call the paint makes) and applies the placements at once, so a
|
||||
string's icons draw with its text wherever it was rebuilt (a chat line laid
|
||||
out by its frame, a tooltip line, a rect change). The paint-pass co-hook
|
||||
`FUN_005c8fe0` still walks the icon records and queues them to
|
||||
`Text::InlineTexturePool` for the next frame tick: the backstop for a string
|
||||
whose placement can't be computed at rebuild time.
|
||||
- **Placement coordinates** — text pen space IS render-target pixels. The
|
||||
engine stores node positions NORMALIZED (fs-local anchor ÷
|
||||
`[0x832A44]`/`[0x832A48]` per axis, `FUN_0041ade0`), and the origin finalize
|
||||
@@ -107,10 +105,8 @@ Every fs-level measure path now counts icons: width (`FUN_00772890`), height
|
||||
refute that: the GameTooltip auto-size measures each WRAPPED SEGMENT of a
|
||||
wrap-enabled line through it, so icon-bearing wrapped tooltip lines
|
||||
undersized their tooltip until it was hooked too). The Lua control surface is
|
||||
two toggles: `_classicapi_InlineTexEnable` (the whole-feature kill switch; the
|
||||
SEH latch trips it on a flush fault) and `_classicapi_InlineTexEarly` (the
|
||||
SMF-refresh early apply above — on by default, toggle off to A/B the chat lag
|
||||
against the baseline paint+tick pipeline, which keeps running either way). The
|
||||
one toggle: `_classicapi_InlineTexEnable` (the whole-feature kill switch; the
|
||||
SEH latch trips it on a fault). The
|
||||
bring-up tune / stat / probe functions were removed (`5e5677b`, `6800a9a`).
|
||||
|
||||
## REMOVED (bring-up record) — the raw-GxU-quad rendering primitive
|
||||
|
||||
+3
-3
@@ -178,9 +178,9 @@ behaviour the comment describes (`push 0; mov edx, 1; mov ecx, L`) is
|
||||
| `0x006F4450` | `lua_next` | fastcall | calls `luaH_next` at `0x006FA2A0` | ★ |
|
||||
| `0x006F44E0` | `lua_concat` | fastcall | calls `luaV_concat` at `0x006F84D0` | ★ |
|
||||
| `0x006F4560` | `lua_newuserdata` | fastcall | tag 7; calls `luaS_newudata` at `0x006F9E30` | ★ |
|
||||
| `0x006F45B0` | `lua_getupvalue` | fastcall | reads upvalue from C closure | ◦ |
|
||||
| `0x006F4660` | `lua_setupvalue` | fastcall | | ◦ |
|
||||
| `0x006F46E0` | *internal upvalue helper*| | shared by get/set above | ◦ |
|
||||
| `0x006F45B0` | `lua_pushupvalues` | fastcall | pushes the running C closure's upvalues, returns how many | ★ |
|
||||
| `0x006F4660` | `lua_getupvalue` | fastcall | `(L, funcindex, n)`: pushes upvalue n, returns its name or null | ★ |
|
||||
| `0x006F46E0` | `aux_upvalue` | | finds upvalue n of a function, returns its name | ★ |
|
||||
|
||||
## Auxiliary library (`luaL_*`)
|
||||
|
||||
|
||||
@@ -64,6 +64,17 @@ bool RedirectJcc(uintptr_t site, uintptr_t expected, const void *target) {
|
||||
return PatchBytes(reinterpret_cast<uint8_t *>(site) + 2, &rel, sizeof rel);
|
||||
}
|
||||
|
||||
bool RedirectSlot(uintptr_t slot, uintptr_t expected, const void *target) {
|
||||
uintptr_t current = 0;
|
||||
std::memcpy(¤t, reinterpret_cast<const void *>(slot), sizeof current);
|
||||
const uintptr_t to = reinterpret_cast<uintptr_t>(target);
|
||||
if (current == to)
|
||||
return true;
|
||||
if (current != expected)
|
||||
return false;
|
||||
return PatchBytes(reinterpret_cast<void *>(slot), &to, sizeof to);
|
||||
}
|
||||
|
||||
bool ReplaceWithCall(uintptr_t site, const uint8_t *original, size_t len, const void *target) {
|
||||
uint8_t code[64];
|
||||
if (len < 5 || len > sizeof code)
|
||||
@@ -102,4 +113,20 @@ void EnsureParentDirs(const char *relPath) {
|
||||
}
|
||||
}
|
||||
|
||||
char FoldPathChar(char c) {
|
||||
if (c == '/')
|
||||
return '\\';
|
||||
if (c >= 'A' && c <= 'Z')
|
||||
return static_cast<char>(c + ('a' - 'A'));
|
||||
return c;
|
||||
}
|
||||
|
||||
bool SamePath(const char *a, const char *b) {
|
||||
for (; FoldPathChar(*a) == FoldPathChar(*b); ++a, ++b) {
|
||||
if (*a == '\0')
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace Common
|
||||
|
||||
@@ -28,6 +28,10 @@ bool RedirectCall(uintptr_t site, uintptr_t expected, const void *target);
|
||||
// at `site` to `target` when it still reaches `expected`. Same contract.
|
||||
bool RedirectJcc(uintptr_t site, uintptr_t expected, const void *target);
|
||||
|
||||
// RedirectCall for a function-pointer slot (a vtable entry): points the slot
|
||||
// at `slot` to `target` when it still holds `expected`. Same contract.
|
||||
bool RedirectSlot(uintptr_t slot, uintptr_t expected, const void *target);
|
||||
|
||||
// Writes `call target` plus NOP padding over the `len` bytes (5 to 64) of
|
||||
// code at `site` when they still hold `original`; true when the site
|
||||
// now calls `target`. A site holding anything else (another DLL's patch, a
|
||||
@@ -45,4 +49,11 @@ void BoundedCopy(char *dst, const char *src, size_t n);
|
||||
// directory. Directories that already exist are a no-op.
|
||||
void EnsureParentDirs(const char *relPath);
|
||||
|
||||
// `c` as game paths compare it: '/' as '\', and A-Z as a-z.
|
||||
char FoldPathChar(char c);
|
||||
|
||||
// Whether `a` and `b` are the same game path: equal after FoldPathChar, so
|
||||
// any case and either slash.
|
||||
bool SamePath(const char *a, const char *b);
|
||||
|
||||
} // namespace Common
|
||||
|
||||
+19
-13
@@ -80,10 +80,13 @@ static void __stdcall LoadGlueScriptFunctions_h() {
|
||||
}
|
||||
|
||||
// Every Lua-side `frame:RegisterEvent(...)` is a chance to claim a slot
|
||||
// for any custom event still waiting. By this point the engine's table
|
||||
// is fully populated and SuperWoWhook / other DLLs have done their
|
||||
// post-rebuild writes, so the table state is settled and our walk from the
|
||||
// low end finds genuinely free slots.
|
||||
// for any custom event still waiting. By the first FrameXML registration
|
||||
// the engine's table is fully populated and SuperWoWhook / other DLLs have
|
||||
// done their post-rebuild writes, so the walk from the low end finds
|
||||
// genuinely free slots. A registration that arrives EARLIER (another DLL's
|
||||
// frame, between LoadScriptFunctions and the table build — issue #134)
|
||||
// still sees glue's 26-entry table; `RetryClaims` refuses that table and
|
||||
// re-validates its cached slots against the live one on every call.
|
||||
static void __fastcall FrameRegisterEvent_h(void *frame, void *edx,
|
||||
const char *eventName) {
|
||||
Event::Custom::RetryClaims();
|
||||
@@ -234,16 +237,19 @@ BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID lpReserved) {
|
||||
CloseHandle(worker);
|
||||
}
|
||||
} else if (reason == DLL_PROCESS_DETACH) {
|
||||
// Clean /quit path — only tear down if we actually initialized.
|
||||
// Flush before MH_Uninitialize — the cache's file I/O uses Win32
|
||||
// directly (no MinHook involvement), so either order works in
|
||||
// practice, but flushing first lets us bail early if the hook
|
||||
// teardown ever grows side effects. Hard process termination (task
|
||||
// manager kill) bypasses this path; that's an inherent OS
|
||||
// limitation, and the 5-minute backstop flush in Remember() covers
|
||||
// the worst case there.
|
||||
// Process exit — only tear down if we actually initialized. A
|
||||
// clean /quit already saved the name cache from its atexit handler,
|
||||
// which runs before this; this save covers an exit that skips the
|
||||
// atexit handlers (Storm's fatal exit 0x0064CC80 calls ExitProcess
|
||||
// itself) and a save that failed there. Save before
|
||||
// MH_Uninitialize — the cache's file I/O uses Win32 directly (no
|
||||
// MinHook involvement), so either order works in practice, but
|
||||
// saving first lets us bail early if the hook teardown ever grows
|
||||
// side effects. Hard process termination (task manager kill)
|
||||
// bypasses this path; that's an inherent OS limitation, and the
|
||||
// 5-minute backstop flush covers the worst case there.
|
||||
if (g_mhInitialized) {
|
||||
Player::NameCache::Flush();
|
||||
Player::NameCache::Shutdown();
|
||||
MH_Uninitialize();
|
||||
}
|
||||
}
|
||||
|
||||
+54
-15
@@ -15,6 +15,7 @@
|
||||
#include "MinHook.h"
|
||||
#include "Offsets.h"
|
||||
#include "api/Documentation.h"
|
||||
#include "compat/WowTurbo.h"
|
||||
|
||||
namespace Game {
|
||||
|
||||
@@ -29,15 +30,23 @@ float PixelToInternal(float px) {
|
||||
const float div = Read<float>(Offsets::VAR_COORD_ASPECT_COMPENSATION);
|
||||
if (div == 0.0f)
|
||||
return 0.0f;
|
||||
return px * mul / (div * static_cast<float>(Offsets::UI_COORD_SCALE_UNIT));
|
||||
return px * mul / (div * Read<float>(Offsets::FLOAT_SCRIPT_COORD_SCALE));
|
||||
}
|
||||
|
||||
double InternalToPixel(float internal) {
|
||||
const float mul = Read<float>(Offsets::VAR_COORD_DDC_WIDTH_SCALE);
|
||||
const float div = Read<float>(Offsets::VAR_COORD_ASPECT_COMPENSATION);
|
||||
if (mul == 0.0f)
|
||||
using GetScaledScreenAspect_t = float(__cdecl *)();
|
||||
// Declared double so the quotient is read from ST0, as the engine's
|
||||
// pushes store it with `fstp qword`, not narrowed to float.
|
||||
using DivideByScreenScaleX_t = double(__stdcall *)(float);
|
||||
if (Read<float>(Offsets::VAR_COORD_DDC_WIDTH_SCALE) == 0.0f)
|
||||
return 0.0;
|
||||
return static_cast<double>(internal) * div * Offsets::UI_COORD_SCALE_UNIT / mul;
|
||||
// `aspect * 1024` is exact, so the float product rounds once, as the
|
||||
// engine's `fstp dword` does.
|
||||
const float aspect =
|
||||
reinterpret_cast<GetScaledScreenAspect_t>(Offsets::FUN_COORD_GET_SCALED_SCREEN_ASPECT)();
|
||||
const float scaled = aspect * Read<float>(Offsets::FLOAT_SCRIPT_COORD_SCALE) * internal;
|
||||
return reinterpret_cast<DivideByScreenScaleX_t>(Offsets::FUN_COORD_DIVIDE_BY_SCREEN_SCALE_X)(
|
||||
scaled);
|
||||
}
|
||||
|
||||
bool StringToFlag(const char *text) {
|
||||
@@ -72,12 +81,14 @@ namespace Lua {
|
||||
F(GetTable, lua_gettable, LUA_GET_TABLE) \
|
||||
F(RawGet, lua_rawget, LUA_RAW_GET) \
|
||||
F(RawGetI, lua_rawgeti, LUA_RAWGETI) \
|
||||
F(RawSetI, lua_rawseti, LUA_RAW_SET_I) \
|
||||
F(SetTable, lua_settable, LUA_SET_TABLE) \
|
||||
F(RawSet, lua_rawset, LUA_RAW_SET) \
|
||||
F(GetMetatable,lua_getmetatable,LUA_GET_METATABLE)\
|
||||
F(SetMetatable,lua_setmetatable,LUA_SET_METATABLE)\
|
||||
F(Insert, lua_insert, LUA_INSERT) \
|
||||
F(Remove, lua_remove, LUA_REMOVE) \
|
||||
F(Replace, lua_replace, LUA_REPLACE) \
|
||||
F(GetTop, lua_gettop, LUA_GET_TOP) \
|
||||
F(SetTop, lua_settop, LUA_SET_TOP) \
|
||||
F(Call, lua_call, LUA_CALL) \
|
||||
@@ -89,16 +100,24 @@ namespace Lua {
|
||||
F(ToPointer, lua_topointer, LUA_TO_POINTER) \
|
||||
F(ToThread, lua_tothread, LUA_TO_THREAD) \
|
||||
F(IsCFunction, lua_iscfunction, LUA_IS_C_FUNCTION)\
|
||||
F(ToCFunction, lua_tocfunction, LUA_TO_C_FUNCTION)\
|
||||
F(GetStack, lua_getstack, LUA_GET_STACK) \
|
||||
F(GetInfo, lua_getinfo, LUA_GET_INFO) \
|
||||
F(XMove, lua_xmove, LUA_X_MOVE) \
|
||||
F(NewThread, lua_newthread, LUA_NEW_THREAD) \
|
||||
F(Resume, lua_resume, LUA_RESUME) \
|
||||
F(Yield, lua_yield, LUA_YIELD) \
|
||||
F(ArgError, luaL_argerror, LUAL_ARG_ERROR) \
|
||||
F(CheckLString,luaL_checklstring,LUAL_CHECK_LSTRING)\
|
||||
F(CheckNumber, luaL_checknumber,LUAL_CHECK_NUMBER)\
|
||||
F(OptNumber, luaL_optnumber, LUAL_OPT_NUMBER) \
|
||||
F(SetN, luaL_setn, LUAL_SETN) \
|
||||
F(Ref, luaL_ref, LUA_REF_REF) \
|
||||
F(Unref, luaL_unref, LUA_REF_UNREF) \
|
||||
F(CheckStack, lua_checkstack, LUA_CHECK_STACK) \
|
||||
F(Concat, lua_concat, LUA_CONCAT)
|
||||
F(Concat, lua_concat, LUA_CONCAT) \
|
||||
F(NewUserdata, lua_newuserdata, LUA_NEW_USERDATA) \
|
||||
F(ToUserdata, lua_touserdata, FUN_FRAMESCRIPT_GET_OBJECT)
|
||||
|
||||
#define CLASSICAPI_BIND_LUA(Name, Typedef, Offset) \
|
||||
const Typedef##_t Name = reinterpret_cast<Typedef##_t>(Offsets::Offset);
|
||||
@@ -241,6 +260,12 @@ void *ResolveEditBox(void *L, int idx, bool raiseError) {
|
||||
void *ResolveModel(void *L, int idx, bool raiseError) {
|
||||
return ResolveTypedObject(L, idx, Offsets::VAR_MODEL_LUA_TYPE_ID, raiseError);
|
||||
}
|
||||
void *ResolvePlayerModel(void *L, int idx, bool raiseError) {
|
||||
return ResolveTypedObject(L, idx, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID, raiseError);
|
||||
}
|
||||
void *ResolveDressUpModel(void *L, int idx, bool raiseError) {
|
||||
return ResolveTypedObject(L, idx, Offsets::VAR_DRESSUPMODEL_LUA_TYPE_ID, raiseError);
|
||||
}
|
||||
|
||||
// Looks up `_G[name]`. If absent, creates a fresh table and binds it.
|
||||
// Leaves the resulting table on top of the stack.
|
||||
@@ -350,11 +375,11 @@ void RegisterGlueFunction(const char *name, CFunction func, const Doc::Function
|
||||
Api::Documentation::RecordGlobal(name, doc);
|
||||
}
|
||||
|
||||
void RegisterFrameMethods(void *context, const FrameMethodEntry *table, int count,
|
||||
const Doc::Method *docs, int docCount) {
|
||||
void RegisterFrameMethods(void *context, ArrayRef<FrameMethodEntry> methods,
|
||||
Doc::MethodList docs) {
|
||||
auto fn = reinterpret_cast<RegisterFrameMethods_t>(Offsets::FUN_REGISTER_FRAME_METHODS);
|
||||
fn(table, count, context);
|
||||
Api::Documentation::RecordFrameMethods(context, table, count, docs, docCount);
|
||||
fn(methods.data, methods.count, context);
|
||||
Api::Documentation::RecordFrameMethods(context, methods, docs);
|
||||
}
|
||||
|
||||
// Registers `func` at `_G[tableName][methodName]`. If the namespace
|
||||
@@ -401,12 +426,12 @@ void RegisterGlobalAlias(const char *alias, const char *tableName, const char *m
|
||||
Api::Documentation::RecordGlobal(alias, doc);
|
||||
}
|
||||
|
||||
void RegisterIntegerEnum(const char *parent, const char *sub,
|
||||
const EnumIntegerEntry *entries, int count,
|
||||
const char *docSystem) {
|
||||
// `_G[parent][sub] = { entries }`, creating `_G[parent]` if needed.
|
||||
static bool SetIntegerTable(const char *parent, const char *sub,
|
||||
const EnumIntegerEntry *entries, int count) {
|
||||
void *L = State();
|
||||
if (L == nullptr)
|
||||
return;
|
||||
return false;
|
||||
EnsureGlobalTable(L, parent); // [parentTbl]
|
||||
PushString(L, sub); // [parentTbl, subName]
|
||||
NewTable(L); // [parentTbl, subName, subTbl]
|
||||
@@ -417,7 +442,19 @@ void RegisterIntegerEnum(const char *parent, const char *sub,
|
||||
}
|
||||
SetTable(L, -3); // parentTbl[sub] = subTbl
|
||||
SetTop(L, -2); // pop parentTbl
|
||||
Api::Documentation::RecordEnum(parent, sub, entries, count, docSystem);
|
||||
return true;
|
||||
}
|
||||
|
||||
void RegisterIntegerEnum(const char *parent, const char *sub, ArrayRef<EnumIntegerEntry> entries,
|
||||
const char *docSystem) {
|
||||
if (SetIntegerTable(parent, sub, entries.data, entries.count))
|
||||
Api::Documentation::RecordEnum(parent, sub, entries, docSystem);
|
||||
}
|
||||
|
||||
void RegisterConstants(const char *sub, ArrayRef<EnumIntegerEntry> entries,
|
||||
const char *docSystem) {
|
||||
if (SetIntegerTable("Constants", sub, entries.data, entries.count))
|
||||
Api::Documentation::RecordConstants(sub, entries, docSystem);
|
||||
}
|
||||
|
||||
void SetFieldNumber(void *L, const char *key, double value) {
|
||||
@@ -568,6 +605,8 @@ HookAutoRegister::HookAutoRegister(uintptr_t target, void *hook, void **original
|
||||
bool RunHookRegistrations() {
|
||||
for (auto *node = g_hookHead; node != nullptr; node = node->next) {
|
||||
auto *targetPtr = reinterpret_cast<LPVOID>(node->target);
|
||||
// A hook must wrap the engine's body, not wow_turbo's reimplementation.
|
||||
Compat::WowTurbo::Reclaim(node->target);
|
||||
if (MH_CreateHook(targetPtr, node->hook,
|
||||
reinterpret_cast<LPVOID *>(node->original)) != MH_OK)
|
||||
return false;
|
||||
|
||||
+85
-20
@@ -58,6 +58,18 @@ inline T &Ref(uintptr_t address) {
|
||||
return *reinterpret_cast<T *>(address);
|
||||
}
|
||||
|
||||
// An engine constant at a fixed VA, read each time it is used, so the value is
|
||||
// the one the engine's own code reads: a runtime patch shows, and so does a
|
||||
// global the engine's static initializers fill after this DLL loads. Converts
|
||||
// to T, so it stands in a float expression as a float would.
|
||||
//
|
||||
// const Game::Live<float> kSkin{Offsets::VAR_MOVER_SKIN};
|
||||
template <typename T>
|
||||
struct Live {
|
||||
uintptr_t address;
|
||||
operator T() const { return Read<T>(address); }
|
||||
};
|
||||
|
||||
// Calls `fn(object)` for each object of one of a frame's intrusive lists,
|
||||
// OFF_FRAME_REGION_LIST or OFF_FRAME_CHILD_LIST: node+4 next, node+8 object,
|
||||
// a NULL or low-bit-1 link ends it - the walk the engine's own frame code
|
||||
@@ -89,8 +101,10 @@ void *ResolveUnitToken(const char *token);
|
||||
|
||||
// UI pixels <-> internal layout (anchor) units. Script_SetPoint converts its
|
||||
// pixel offsets with `px * [VAR_COORD_DDC_WIDTH_SCALE] / ([VAR_COORD_ASPECT_COMPENSATION]
|
||||
// * UI_COORD_SCALE_UNIT)`; Script_GetLeft and Script_GetStringWidth push the
|
||||
// inverse. A region's LOCAL pixels must be multiplied by its effective scale
|
||||
// * [FLOAT_SCRIPT_COORD_SCALE])`; every script coordinate push (GetLeft,
|
||||
// GetStringWidth, GetFont's height, GetCursorPosition, OnCursorChanged) is the
|
||||
// inverse, `DivideByScreenScaleX(GetScaledScreenAspect() * 1024 * value)`,
|
||||
// which InternalToPixel calls in that order. A region's LOCAL pixels must be multiplied by its effective scale
|
||||
// (OFF_REGION_EFFECTIVE_SCALE) first. Both return 0 while the scale globals
|
||||
// are unset, so an early caller gets a zero offset rather than a wild one.
|
||||
float PixelToInternal(float px);
|
||||
@@ -147,6 +161,18 @@ int StrCmpI(const char *a, const char *b, int n = 0x7FFFFFFF);
|
||||
// literals + file-scope arrays): no heap, no dynamic init.
|
||||
// - Summaries are user-facing text: one sentence, plain words, present
|
||||
// tense, no patch versions.
|
||||
|
||||
// A view over a file-scope `const T[]` that takes its count from the array's
|
||||
// type, so no call site passes one; `{}` means none.
|
||||
template <typename T>
|
||||
struct ArrayRef {
|
||||
const T *data = nullptr;
|
||||
int count = 0;
|
||||
constexpr ArrayRef() = default;
|
||||
template <size_t N>
|
||||
constexpr ArrayRef(const T (&a)[N]) : data(a), count(static_cast<int>(N)) {}
|
||||
};
|
||||
|
||||
namespace Doc {
|
||||
|
||||
struct Field {
|
||||
@@ -172,14 +198,7 @@ constexpr Field Vararg(const char *type, const char *doc = nullptr) {
|
||||
return {"...", type, true, nullptr, doc};
|
||||
}
|
||||
|
||||
// A view over a file-scope `const Field[]`; `{}` means none.
|
||||
struct FieldList {
|
||||
const Field *data = nullptr;
|
||||
int count = 0;
|
||||
constexpr FieldList() = default;
|
||||
template <size_t N>
|
||||
constexpr FieldList(const Field (&a)[N]) : data(a), count(static_cast<int>(N)) {}
|
||||
};
|
||||
using FieldList = ArrayRef<Field>;
|
||||
|
||||
// A registered function. Positional init: `{summary, args, rets}` for a
|
||||
// table function, `{summary, args, rets, "XGlobals"}` for a global, and
|
||||
@@ -200,6 +219,7 @@ struct Method {
|
||||
const char *name;
|
||||
const Function *doc;
|
||||
};
|
||||
using MethodList = ArrayRef<Method>;
|
||||
|
||||
// A table shape a function returns or accepts → `Tables{Type="Structure"}`
|
||||
// in `system`. Self-registering: nothing binds a structure to Lua, so it
|
||||
@@ -240,7 +260,9 @@ constexpr int UpvalueIndex(int i) { return GLOBALS_INDEX - i; }
|
||||
// `lua_call` / `lua_pcall` nresults sentinel meaning "all".
|
||||
constexpr int MULTRET = -1;
|
||||
|
||||
// Type tag values returned by `Type()` (lua_type).
|
||||
// Type tag values returned by `Type()` (lua_type). TYPE_NONE is an absent
|
||||
// argument (past the top of the stack).
|
||||
constexpr int TYPE_NONE = -1;
|
||||
constexpr int TYPE_NIL = 0;
|
||||
constexpr int TYPE_BOOLEAN = 1;
|
||||
constexpr int TYPE_LIGHTUSERDATA = 2;
|
||||
@@ -270,6 +292,8 @@ using lua_rawget_t = void(__fastcall *)(void *L, int idx);
|
||||
// Pushes t[n] for the table at idx, raw (no metamethods). With idx =
|
||||
// REGISTRY_INDEX it pushes a value stored under an integer ref.
|
||||
using lua_rawgeti_t = void(__fastcall *)(void *L, int idx, int n);
|
||||
// Pops the top value into t[n] for the table at idx, raw.
|
||||
using lua_rawseti_t = void(__fastcall *)(void *L, int idx, int n);
|
||||
using lua_settable_t = void(__fastcall *)(void *L, int idx);
|
||||
using lua_rawset_t = void(__fastcall *)(void *L, int idx);
|
||||
// Pushes the metatable of the value at idx and returns 1, or pushes
|
||||
@@ -280,6 +304,8 @@ using lua_getmetatable_t = int(__fastcall *)(void *L, int idx);
|
||||
using lua_setmetatable_t = int(__fastcall *)(void *L, int idx);
|
||||
using lua_insert_t = void(__fastcall *)(void *L, int idx);
|
||||
using lua_remove_t = void(__fastcall *)(void *L, int idx);
|
||||
// Pops the top value into slot idx.
|
||||
using lua_replace_t = void(__fastcall *)(void *L, int idx);
|
||||
using lua_gettop_t = int(__fastcall *)(void *L);
|
||||
using lua_settop_t = void(__fastcall *)(void *L, int idx);
|
||||
using lua_call_t = void(__fastcall *)(void *L, int nargs, int nresults);
|
||||
@@ -298,6 +324,11 @@ using lua_error_t = void(__cdecl *)(void *L, const char *fmt, ...);
|
||||
using lua_topointer_t = const void *(__fastcall *)(void *L, int idx);
|
||||
using lua_tothread_t = void *(__fastcall *)(void *L, int idx);
|
||||
using lua_iscfunction_t = int(__fastcall *)(void *L, int idx);
|
||||
using lua_tocfunction_t = CFunction(__fastcall *)(void *L, int idx);
|
||||
// Fill a lua_Debug (Offsets::SIZEOF_LUA_DEBUG bytes) for a call level /
|
||||
// with the fields `what` names. Level 0 is the running C function.
|
||||
using lua_getstack_t = int(__fastcall *)(void *L, int level, void *ar);
|
||||
using lua_getinfo_t = int(__fastcall *)(void *L, const char *what, void *ar);
|
||||
using lua_xmove_t = void(__fastcall *)(void *from, void *to, int n);
|
||||
// `lua_newthread` returns the new `lua_State *` in eax, but Ghidra
|
||||
// infers `void` because the inner call's value just flows through.
|
||||
@@ -308,6 +339,11 @@ using lua_newthread_t = void *(__fastcall *)(void *L);
|
||||
using lua_resume_t = int(__fastcall *)(void *L, int nargs);
|
||||
using lua_yield_t = int(__fastcall *)(void *L, int nresults);
|
||||
using luaL_argerror_t = void(__fastcall *)(void *L, int narg, const char *msg);
|
||||
// The auxlib argument readers: each raises the engine's own "bad argument"
|
||||
// error. CheckLString's `len` may be null.
|
||||
using luaL_checklstring_t = const char *(__fastcall *)(void *L, int narg, unsigned int *len);
|
||||
using luaL_checknumber_t = double(__fastcall *)(void *L, int narg);
|
||||
using luaL_optnumber_t = double(__fastcall *)(void *L, int narg, double def);
|
||||
using luaL_setn_t = void(__fastcall *)(void *L, int t, int n);
|
||||
// Pops the top value into the table at `t` under a fresh integer key and
|
||||
// returns the key (LUA_REFNIL, -1, for a nil). With REGISTRY_INDEX it anchors
|
||||
@@ -319,6 +355,10 @@ using lua_checkstack_t = int(__fastcall *)(void *L, int size);
|
||||
// Pops the top `n` values and pushes their `..` concatenation; n == 0 pushes
|
||||
// "" and n == 1 leaves the value untouched.
|
||||
using lua_concat_t = void(__fastcall *)(void *L, int n);
|
||||
// Pushes a new full userdata of `size` bytes and returns its block.
|
||||
using lua_newuserdata_t = void *(__fastcall *)(void *L, unsigned int size);
|
||||
// The block of the full userdata at idx, the pointer of a light one, else null.
|
||||
using lua_touserdata_t = void *(__fastcall *)(void *L, int idx);
|
||||
|
||||
extern const lua_isnumber_t IsNumber;
|
||||
extern const lua_isstring_t IsString;
|
||||
@@ -349,12 +389,14 @@ extern const lua_newtable_t NewTable;
|
||||
extern const lua_gettable_t GetTable;
|
||||
extern const lua_rawget_t RawGet;
|
||||
extern const lua_rawgeti_t RawGetI;
|
||||
extern const lua_rawseti_t RawSetI;
|
||||
extern const lua_settable_t SetTable;
|
||||
extern const lua_rawset_t RawSet;
|
||||
extern const lua_getmetatable_t GetMetatable;
|
||||
extern const lua_setmetatable_t SetMetatable;
|
||||
extern const lua_insert_t Insert;
|
||||
extern const lua_remove_t Remove;
|
||||
extern const lua_replace_t Replace;
|
||||
extern const lua_gettop_t GetTop;
|
||||
extern const lua_settop_t SetTop;
|
||||
extern const lua_call_t Call;
|
||||
@@ -366,16 +408,24 @@ extern const lua_error_t Error;
|
||||
extern const lua_topointer_t ToPointer;
|
||||
extern const lua_tothread_t ToThread;
|
||||
extern const lua_iscfunction_t IsCFunction;
|
||||
extern const lua_tocfunction_t ToCFunction;
|
||||
extern const lua_getstack_t GetStack;
|
||||
extern const lua_getinfo_t GetInfo;
|
||||
extern const lua_xmove_t XMove;
|
||||
extern const lua_newthread_t NewThread;
|
||||
extern const lua_resume_t Resume;
|
||||
extern const lua_yield_t Yield;
|
||||
extern const luaL_argerror_t ArgError;
|
||||
extern const luaL_checklstring_t CheckLString;
|
||||
extern const luaL_checknumber_t CheckNumber;
|
||||
extern const luaL_optnumber_t OptNumber;
|
||||
extern const luaL_setn_t SetN;
|
||||
extern const luaL_ref_t Ref;
|
||||
extern const luaL_unref_t Unref;
|
||||
extern const lua_checkstack_t CheckStack;
|
||||
extern const lua_concat_t Concat;
|
||||
extern const lua_newuserdata_t NewUserdata;
|
||||
extern const lua_touserdata_t ToUserdata;
|
||||
|
||||
// Returns the global `lua_State *` (read on demand from the engine's global).
|
||||
// Callable outside a Lua callback, e.g. during LoadScriptFunctions setup.
|
||||
@@ -451,6 +501,8 @@ void *ResolveTexture(void *L, int idx = 1, bool raiseError = true);
|
||||
void *ResolveFontString(void *L, int idx = 1, bool raiseError = true);
|
||||
void *ResolveEditBox(void *L, int idx = 1, bool raiseError = true);
|
||||
void *ResolveModel(void *L, int idx = 1, bool raiseError = true);
|
||||
void *ResolvePlayerModel(void *L, int idx = 1, bool raiseError = true);
|
||||
void *ResolveDressUpModel(void *L, int idx = 1, bool raiseError = true);
|
||||
|
||||
// Every name these registrars bind is ALSO bound under `_G.ClassicAPI` —
|
||||
// `ClassicAPI.GetSpellInfo`, `ClassicAPI.C_Item.IsBound` — as an escape
|
||||
@@ -495,10 +547,11 @@ struct FrameMethodEntry {
|
||||
|
||||
// Registers a batch of methods on a per-frame-type registry (e.g.
|
||||
// GameTooltipMethodRegistry for `tooltip:Foo()` calls). `context` is the
|
||||
// registry address — see Offsets::VAR_*_METHOD_REGISTRY. `docs` pairs
|
||||
// descriptors to the entries by NAME (any order, any subset).
|
||||
void RegisterFrameMethods(void *context, const FrameMethodEntry *table, int count,
|
||||
const Doc::Method *docs = nullptr, int docCount = 0);
|
||||
// registry address — see Offsets::VAR_*_METHOD_REGISTRY. `methods` and
|
||||
// `docs` are file-scope arrays; `docs` pairs descriptors to the entries by
|
||||
// NAME (any order, any subset).
|
||||
void RegisterFrameMethods(void *context, ArrayRef<FrameMethodEntry> methods,
|
||||
Doc::MethodList docs = {});
|
||||
|
||||
// Registers `func` at `_G[tableName][methodName]`, creating the namespace
|
||||
// table if it doesn't already exist. This is how modern WoW C_*-style APIs
|
||||
@@ -516,9 +569,9 @@ void RegisterTableFunction(const char *tableName, const char *methodName,
|
||||
void RegisterGlobalAlias(const char *alias, const char *tableName, const char *methodName,
|
||||
const Doc::Function *doc = nullptr);
|
||||
|
||||
// Key/value pair for `RegisterIntegerEnum`. `key` becomes a field name
|
||||
// (PascalCase, matching Blizzard's `Enum.*` naming) and `value` is the
|
||||
// integer the enum field maps to.
|
||||
// Key/value pair for `RegisterIntegerEnum` and `RegisterConstants`. `key`
|
||||
// becomes a field name (PascalCase for Blizzard's `Enum.*`, UPPER_CASE for
|
||||
// its `Constants.*`) and `value` is the integer the field maps to.
|
||||
struct EnumIntegerEntry {
|
||||
const char *key;
|
||||
int value;
|
||||
@@ -529,10 +582,16 @@ struct EnumIntegerEntry {
|
||||
// `Enum.AddOnSecurityStatus = { Secure=0, Insecure=1, ... }` shapes.
|
||||
// `docSystem` is the documentation System the enumeration belongs to
|
||||
// ("SpellBook", "Item", …); the entries themselves are the documentation.
|
||||
void RegisterIntegerEnum(const char *parent, const char *sub,
|
||||
const EnumIntegerEntry *entries, int count,
|
||||
// `entries` is a file-scope array.
|
||||
void RegisterIntegerEnum(const char *parent, const char *sub, ArrayRef<EnumIntegerEntry> entries,
|
||||
const char *docSystem = nullptr);
|
||||
|
||||
// Registers `Constants[sub] = { entries }`, Blizzard's engine constant
|
||||
// tables (`Constants.MoneyFormattingConstants`). Documented as a "Constants"
|
||||
// table of `docSystem`.
|
||||
void RegisterConstants(const char *sub, ArrayRef<EnumIntegerEntry> entries,
|
||||
const char *docSystem);
|
||||
|
||||
// Set `t[key] = value` on the table currently at stack[-1] (the most
|
||||
// common shape used when populating a struct-style table mid-build).
|
||||
// Pushes the key + value, calls `SetTable(L, -3)`, which pops both and
|
||||
@@ -743,6 +802,12 @@ bool RunHookRegistrations();
|
||||
// issue #33, Frame::Attributes). A pure-C++ cache keyed by spellID/itemID is
|
||||
// NOT fragile and must not register.
|
||||
//
|
||||
// It runs AFTER the frames are gone: CGGameUI::Shutdown destroys every frame
|
||||
// (and each Model frame's scene with every model in it) before
|
||||
// CGGameUI::Initialize resets the Lua state. So a cleanup here only FORGETS
|
||||
// pointers; it never releases an engine object tied to a frame. Release those
|
||||
// when the frame dies (Model::Teardown for Model frames).
|
||||
//
|
||||
// Same lifetime + ordering rules as ModuleAutoRegister: static-init chaining,
|
||||
// undefined cross-TU order — fine, because each callback only clears its OWN
|
||||
// state. This is the "clear stale state" category only; a save-before-teardown
|
||||
|
||||
@@ -32,9 +32,8 @@ namespace {
|
||||
// that are not base-library functions at all. So do not derive this from
|
||||
// the engine's `luaopen_base` table (`0x00811E28`, 36 entries — see
|
||||
// BlizzardScriptAPI.md §3); that is a different set with a different
|
||||
// purpose. Names with no 1.12 counterpart (`wipe` is `table.wipe` here; the
|
||||
// secure family does not exist) stay for parity of the error an addon
|
||||
// sees. Sorted alphabetically.
|
||||
// purpose. Names with no 1.12 counterpart (the secure family does not
|
||||
// exist) stay for parity of the error an addon sees. Sorted alphabetically.
|
||||
const char *const kUnhookableNames[] = {
|
||||
"getfenv", "getmetatable", "hooksecurefunc", "ipairs", "issecurevalue",
|
||||
"issecurevariable", "next", "pairs", "pcall", "pcallwithenv", "rawget",
|
||||
|
||||
+3746
-181
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,178 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Where a spell sits on the bars.
|
||||
//
|
||||
// FindSpellActionButtons(spell [, set]) the 1-based slots holding the
|
||||
// spell as a spell action, or nothing when none does.
|
||||
// HasSpellActionButtons(spell [, set]) whether any slot does.
|
||||
// IsOnBarOrSpecialBar(spell [, set]) the same, or the spell is on the
|
||||
// shapeshift bar or the pet bar.
|
||||
//
|
||||
// A slot matches on the spell ID it stores, as 4.3.4's PutActionInSlot
|
||||
// compares (IsSpell && GetSpell == spellId): a macro or an item that casts
|
||||
// the spell is not the spell, and a rank matches only itself. The special
|
||||
// bars are the engine's own lists — the shapeshift bar's spell array that
|
||||
// GetShapeshiftFormInfo reads, and the pet bar's spell entries
|
||||
// (GetPetActionInfo's types 1-5) while a pet bar is up.
|
||||
//
|
||||
// `set` is Enum.ActionBarSet, a mask whose Mkb bit is the one set of slots
|
||||
// this client has; Gamepad alone finds no slot.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "spell/Arg.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Action::Find {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int ACTION_BAR_SET_MKB = 1;
|
||||
constexpr int ACTION_BAR_SET_GAMEPAD = 2;
|
||||
constexpr int ACTION_BAR_SET_ALL = ACTION_BAR_SET_MKB | ACTION_BAR_SET_GAMEPAD;
|
||||
|
||||
// The spell at stack[1] and whether the set at stack[2] (default All)
|
||||
// covers this client's slots. 0 when the spell does not resolve.
|
||||
int Args(void *L, bool *searchSlots) {
|
||||
const int set = Game::Lua::IsNumber(L, 2) ? static_cast<int>(Game::Lua::ToNumber(L, 2))
|
||||
: ACTION_BAR_SET_ALL;
|
||||
*searchSlots = (set & ACTION_BAR_SET_MKB) != 0;
|
||||
return Spell::Arg::ResolveSpellID(L, 1);
|
||||
}
|
||||
|
||||
bool SlotHolds(int slot0, int spellID) {
|
||||
const uint32_t entry = reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_ACTION_TABLE))[slot0];
|
||||
return (entry & Offsets::ACTION_TYPE_MASK) == Offsets::ACTION_TYPE_SPELL &&
|
||||
entry == static_cast<uint32_t>(spellID);
|
||||
}
|
||||
|
||||
bool AnySlotHolds(int spellID) {
|
||||
for (int slot0 = 0; slot0 < Offsets::ACTION_TABLE_MAX_SLOTS; ++slot0) {
|
||||
if (SlotHolds(slot0, spellID))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool OnShapeshiftBar(int spellID) {
|
||||
const int count = Game::Read<int>(Offsets::VAR_SHAPESHIFT_BAR_SPELL_COUNT);
|
||||
const auto *spells = Game::Read<const uint32_t *>(Offsets::VAR_SHAPESHIFT_BAR_SPELLS);
|
||||
if (spells == nullptr)
|
||||
return false;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (spells[i] == static_cast<uint32_t>(spellID))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool OnPetBar(int spellID) {
|
||||
if (Game::Read<uint64_t>(Offsets::VAR_PET_GUID) == 0)
|
||||
return false;
|
||||
const auto *slots = reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_PET_ACTION_SLOTS));
|
||||
for (int i = 0; i < Offsets::PET_ACTION_SLOT_COUNT; ++i) {
|
||||
const uint32_t type = (slots[i] >> 24) & 0x3F;
|
||||
if (type >= Offsets::PET_ACTION_TYPE_SPELL_FIRST &&
|
||||
type <= Offsets::PET_ACTION_TYPE_SPELL_LAST &&
|
||||
(slots[i] & 0xFFFF) == static_cast<uint32_t>(spellID))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int __fastcall Script_FindSpellActionButtons(void *L) {
|
||||
bool searchSlots;
|
||||
const int spellID = Args(L, &searchSlots);
|
||||
if (spellID <= 0 || !searchSlots || !AnySlotHolds(spellID))
|
||||
return 0;
|
||||
Game::Lua::NewTable(L);
|
||||
int index = 0;
|
||||
for (int slot0 = 0; slot0 < Offsets::ACTION_TABLE_MAX_SLOTS; ++slot0) {
|
||||
if (!SlotHolds(slot0, spellID))
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(++index));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(slot0 + 1));
|
||||
Game::Lua::RawSet(L, -3);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_HasSpellActionButtons(void *L) {
|
||||
bool searchSlots;
|
||||
const int spellID = Args(L, &searchSlots);
|
||||
Game::Lua::PushBool(L, spellID > 0 && searchSlots && AnySlotHolds(spellID));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_IsOnBarOrSpecialBar(void *L) {
|
||||
bool searchSlots;
|
||||
const int spellID = Args(L, &searchSlots);
|
||||
Game::Lua::PushBool(L, spellID > 0 && ((searchSlots && AnySlotHolds(spellID)) ||
|
||||
OnShapeshiftBar(spellID) || OnPetBar(spellID)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// --- Documentation ----------------------------------------------------------
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("spellID", "SpellIdentifier", "A spell ID, spell link, or spell name."),
|
||||
Game::Doc::Opt("setToSearch", "ActionBarSet", "All"),
|
||||
};
|
||||
const Game::Doc::Field kFindRets[] = {
|
||||
Game::Doc::Req("slots", "table", "The 1-based action slots; nothing when no slot "
|
||||
"holds the spell."),
|
||||
};
|
||||
const Game::Doc::Function kFindSpellActionButtons{
|
||||
"The action slots that hold the spell as a spell action.",
|
||||
kArgs, kFindRets};
|
||||
|
||||
const Game::Doc::Field kHasRets[] = {
|
||||
Game::Doc::Req("hasSpellActionButtons", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kHasSpellActionButtons{
|
||||
"Whether any action slot holds the spell as a spell action.",
|
||||
kArgs, kHasRets};
|
||||
|
||||
const Game::Doc::Field kOnBarRets[] = {
|
||||
Game::Doc::Req("isOnBarOrSpecialBar", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kIsOnBarOrSpecialBar{
|
||||
"Whether the spell is in an action slot, on the shapeshift bar, or on the pet bar.",
|
||||
kArgs, kOnBarRets};
|
||||
|
||||
const Game::Lua::EnumIntegerEntry kActionBarSet[] = {
|
||||
{"None", 0},
|
||||
{"Mkb", ACTION_BAR_SET_MKB},
|
||||
{"Gamepad", ACTION_BAR_SET_GAMEPAD},
|
||||
{"All", ACTION_BAR_SET_ALL},
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_ActionBar", "FindSpellActionButtons",
|
||||
&Script_FindSpellActionButtons, &kFindSpellActionButtons);
|
||||
Game::Lua::RegisterTableFunction("C_ActionBar", "HasSpellActionButtons",
|
||||
&Script_HasSpellActionButtons, &kHasSpellActionButtons);
|
||||
Game::Lua::RegisterTableFunction("C_ActionBar", "IsOnBarOrSpecialBar",
|
||||
&Script_IsOnBarOrSpecialBar, &kIsOnBarOrSpecialBar);
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "ActionBarSet", kActionBarSet, "ActionBar");
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace Action::Find
|
||||
@@ -38,11 +38,24 @@
|
||||
// through to an empty return. There is no bag-item tag: items are always
|
||||
// by-itemID (0x80), and an earlier reading of the 0x40 "miss" as a bag item
|
||||
// was wrong.
|
||||
//
|
||||
// `C_ActionBar.GetSpell(slot)` — the spell the slot stands for, 0 for none.
|
||||
// It is the engine's own resolver (FUN_ACTION_SLOT_TO_SPELL), which every
|
||||
// action reader (cooldown, usable, range, current) goes through: a spell
|
||||
// action is its spell, a macro is its cached primary spell, and an item is
|
||||
// its first on-use spell — retail's ActionButton.lua calls GetSpell for
|
||||
// exactly that item case. The spellbook flag it also hands back is not part
|
||||
// of the answer.
|
||||
//
|
||||
// `C_ActionBar.IsMacroActionWithShowTooltip(slot)` — the slot holds a macro
|
||||
// whose body carries `#showtooltip`, by the parse `Macro::ShowTooltip` hands
|
||||
// the tooltip over on. `#show` is not it.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "action/Slot.h"
|
||||
#include "equipmentset/Data.h"
|
||||
#include "macro/ShowTooltip.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -126,10 +139,58 @@ int __fastcall Script_GetActionInfo(void *L) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetSpell(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: C_ActionBar.GetSpell(actionID)");
|
||||
return 0;
|
||||
}
|
||||
// The resolver bounds-checks the slot itself and answers 0 outside it.
|
||||
const int slot0 = static_cast<int>(Game::Lua::ToNumber(L, 1)) - 1;
|
||||
uint32_t petFlag = 0;
|
||||
const uint32_t spellID = reinterpret_cast<SlotToSpell_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_ACTION_SLOT_TO_SPELL))(
|
||||
static_cast<uint32_t>(slot0), &petFlag);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(spellID));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_IsMacroActionWithShowTooltip(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: C_ActionBar.IsMacroActionWithShowTooltip(actionID)");
|
||||
return 0;
|
||||
}
|
||||
const int slot0 = static_cast<int>(Game::Lua::ToNumber(L, 1)) - 1;
|
||||
const uint32_t macroID = Action::Slot::MacroIDForSlot(slot0);
|
||||
Game::Lua::PushBool(L, macroID != 0 && Macro::ShowTooltip::HasShowTooltipDirective(macroID));
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kGetSpellArgs[] = {
|
||||
Game::Doc::Req("actionID", "luaIndex"),
|
||||
};
|
||||
const Game::Doc::Field kGetSpellRets[] = {
|
||||
Game::Doc::Req("spellID", "number"),
|
||||
};
|
||||
const Game::Doc::Function kGetSpellDoc{
|
||||
"The spell an action stands for: a spell's own ID, a macro's spell, or an item's "
|
||||
"on-use spell; 0 when there is none.",
|
||||
kGetSpellArgs, kGetSpellRets};
|
||||
|
||||
const Game::Doc::Field kIsMacroActionWithShowTooltipRets[] = {
|
||||
Game::Doc::Req("isMacroActionWithShowTooltip", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kIsMacroActionWithShowTooltipDoc{
|
||||
"Whether the action is a macro with an explicit #showtooltip line.",
|
||||
kGetSpellArgs, kIsMacroActionWithShowTooltipRets};
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetActionInfo", &Script_GetActionInfo);
|
||||
Game::Lua::RegisterTableFunction("C_ActionBar", "GetSpell", &Script_GetSpell, &kGetSpellDoc);
|
||||
Game::Lua::RegisterTableFunction("C_ActionBar", "IsMacroActionWithShowTooltip",
|
||||
&Script_IsMacroActionWithShowTooltip,
|
||||
&kIsMacroActionWithShowTooltipDoc);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
// The function's one caller, Script_IsActionInRange, is redirected rather than
|
||||
// the function hooked.
|
||||
|
||||
#include "action/Range.h"
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
@@ -107,4 +109,6 @@ const Game::ModuleAutoRegister _autoreg{&Register};
|
||||
|
||||
} // namespace
|
||||
|
||||
int Answer(int slot0, int *inRange) { return SlotRange_h(slot0, inRange); }
|
||||
|
||||
} // namespace Action::Range
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under
|
||||
// the terms of the GNU General Public License as published by the Free Software
|
||||
// Foundation, either version 3 of the License, or (at your option) any later
|
||||
// version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
// A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Action::Range {
|
||||
|
||||
// IsActionInRange's answer for a slot (0-based, 0..119): 0 when it has none
|
||||
// (IsActionInRange's nil), else 1 with `*inRange` 0 or 1. A macro aimed at a
|
||||
// unit is checked against that unit.
|
||||
int Answer(int slot0, int *inRange);
|
||||
|
||||
} // namespace Action::Range
|
||||
@@ -0,0 +1,144 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under
|
||||
// the terms of the GNU General Public License as published by the Free Software
|
||||
// Foundation, either version 3 of the License, or (at your option) any later
|
||||
// version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
// A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_ActionBar.EnableActionRangeCheck(slot, enable)` and
|
||||
// `ACTION_RANGE_CHECK_UPDATE(slot, isInRange, checksRange)`.
|
||||
//
|
||||
// Nothing in the client reacts to a distance (see Unit::InRangeEvent), so the
|
||||
// enabled slots are checked every world tick, while a frame is registered for
|
||||
// the event, and a slot fires when its answer changes. The answer is
|
||||
// IsActionInRange's (Action::Range::Answer), macro unit included, so a slot
|
||||
// whose action changes fires when the new action's answer differs.
|
||||
//
|
||||
// Enabling is counted per slot, as retail's ActionBarButtonRangeCheckFrame
|
||||
// expects: it enables once for each button on a slot and disables once as
|
||||
// each button leaves. Retail's buttons never call IsActionInRange; they take
|
||||
// their first state from this event. So each enable makes the slot's next
|
||||
// check fire, changed or not. A new Lua state (`/reload`) starts with no slot
|
||||
// enabled.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "action/Range.h"
|
||||
#include "event/Custom.h"
|
||||
#include "tick/WorldTick.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Action::RangeCheck {
|
||||
|
||||
namespace {
|
||||
|
||||
enum class State : uint8_t { NoCheck, OutOfRange, InRange };
|
||||
|
||||
struct Slot {
|
||||
int refs = 0;
|
||||
bool pending = false; // fire on the next check, changed or not
|
||||
State last = State::NoCheck;
|
||||
};
|
||||
|
||||
Slot g_slots[Offsets::ACTION_TABLE_MAX_SLOTS];
|
||||
|
||||
const Game::Doc::Field kPayload[] = {
|
||||
Game::Doc::Req("slot", "luaIndex"),
|
||||
Game::Doc::Req("isInRange", "bool",
|
||||
"Whether the action's target is in range. Should not be used if checksRange "
|
||||
"is false."),
|
||||
Game::Doc::Req("checksRange", "bool",
|
||||
"False when no range check was made, for example when there is no target."),
|
||||
};
|
||||
const Game::Doc::Event kDoc{
|
||||
"ActionBar",
|
||||
"Fires when an action that has EnableActionRangeCheck on goes in or out of range of its "
|
||||
"target.",
|
||||
kPayload};
|
||||
const Event::Custom::AutoReserve kEvent{"ACTION_RANGE_CHECK_UPDATE", &kDoc};
|
||||
|
||||
State Evaluate(int slot0) {
|
||||
int inRange = 0;
|
||||
if (Action::Range::Answer(slot0, &inRange) == 0)
|
||||
return State::NoCheck;
|
||||
return inRange != 0 ? State::InRange : State::OutOfRange;
|
||||
}
|
||||
|
||||
void OnWorldTick() {
|
||||
const int event = kEvent.Slot();
|
||||
if (!Event::Custom::HasListeners(event))
|
||||
return;
|
||||
void *L = Game::Lua::State();
|
||||
if (L == nullptr)
|
||||
return;
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
for (int slot0 = 0; slot0 < Offsets::ACTION_TABLE_MAX_SLOTS; ++slot0) {
|
||||
Slot &slot = g_slots[slot0];
|
||||
if (slot.refs == 0)
|
||||
continue;
|
||||
const State state = Evaluate(slot0);
|
||||
if (!slot.pending && state == slot.last)
|
||||
continue;
|
||||
slot.pending = false;
|
||||
slot.last = state;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(slot0 + 1));
|
||||
Game::Lua::PushBool(L, state == State::InRange);
|
||||
Game::Lua::PushBool(L, state != State::NoCheck);
|
||||
Event::Custom::FireValues(event, L, top + 1, 3);
|
||||
Game::Lua::SetTop(L, top);
|
||||
}
|
||||
}
|
||||
|
||||
const Tick::WorldTick::AutoSubscribe _tick{&OnWorldTick};
|
||||
|
||||
// --- C_ActionBar.EnableActionRangeCheck(slot, enable) -----------------------
|
||||
|
||||
int __fastcall Script_EnableActionRangeCheck(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: C_ActionBar.EnableActionRangeCheck(slot, enable)");
|
||||
return 0;
|
||||
}
|
||||
const int slot0 = static_cast<int>(Game::Lua::ToNumber(L, 1)) - 1;
|
||||
if (slot0 < 0 || slot0 >= Offsets::ACTION_TABLE_MAX_SLOTS)
|
||||
return 0;
|
||||
Slot &slot = g_slots[slot0];
|
||||
if (Game::Lua::ToBoolean(L, 2) != 0) {
|
||||
++slot.refs;
|
||||
slot.pending = true;
|
||||
} else if (slot.refs > 0) {
|
||||
--slot.refs;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kEnableArgs[] = {
|
||||
Game::Doc::Req("slot", "luaIndex"),
|
||||
Game::Doc::Req("enable", "bool",
|
||||
"True if changes in range for the action should dispatch "
|
||||
"ACTION_RANGE_CHECK_UPDATE. False if the action no longer needs the event."),
|
||||
};
|
||||
const Game::Doc::Function kEnableDoc{
|
||||
"Used with ACTION_RANGE_CHECK_UPDATE to be informed when an action goes in or out of range "
|
||||
"of its target.",
|
||||
kEnableArgs};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
for (Slot &slot : g_slots)
|
||||
slot = Slot{};
|
||||
Game::Lua::RegisterTableFunction("C_ActionBar", "EnableActionRangeCheck",
|
||||
&Script_EnableActionRangeCheck, &kEnableDoc);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Action::RangeCheck
|
||||
@@ -0,0 +1,148 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// The item behind an action slot, 4.3.4's CGActionBar::GetItem: an item-by-ID
|
||||
// action's item, or the item a macro's `#showtooltip` / `#show` resolved to
|
||||
// (1.12 keeps no item per macro, so `Macro::ShowTooltip` holds it). Both
|
||||
// readers below peek the item cache and never query it, as 4.3.4 does.
|
||||
//
|
||||
// IsStackableAction the item's max stack is more than 1 —
|
||||
// 4.3.4's IsStackableAucItem. The global
|
||||
// answers 1/nil, the C_ActionBar form a bool.
|
||||
// GetItemActionOnEquipSpellID the item's first on-equip spell (trigger 1),
|
||||
// nil when it has none.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/Record.h"
|
||||
#include "macro/ShowTooltip.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Action::SlotItem {
|
||||
|
||||
namespace {
|
||||
|
||||
uint32_t ItemForSlot(int slot0) {
|
||||
if (slot0 < 0 || slot0 >= Offsets::ACTION_TABLE_MAX_SLOTS)
|
||||
return 0;
|
||||
const uint32_t entry = reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_ACTION_TABLE))[slot0];
|
||||
if ((entry & Offsets::ACTION_TYPE_MASK) == Offsets::ACTION_TYPE_ITEM_BY_ID)
|
||||
return entry & Offsets::ACTION_PAYLOAD_MASK_ITEM_BY_ID;
|
||||
Macro::ShowTooltip::Info info;
|
||||
if (Macro::ShowTooltip::ForSlot(slot0, &info) &&
|
||||
info.target == Macro::ShowTooltip::Target::Item)
|
||||
return static_cast<uint32_t>(info.itemID);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Reads the 1-based slot at stack[1]; false (after the usage error) when it
|
||||
// is not a number.
|
||||
bool SlotArg(void *L, const char *usage, int *slot0) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, usage);
|
||||
return false;
|
||||
}
|
||||
*slot0 = static_cast<int>(Game::Lua::ToNumber(L, 1)) - 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsStackable(int slot0) {
|
||||
const uint8_t *record = ::Item::PeekRecord(ItemForSlot(slot0));
|
||||
return record != nullptr && Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_STACKABLE) > 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_IsStackableAction(void *L) {
|
||||
int slot0;
|
||||
if (!SlotArg(L, "Usage: IsStackableAction(slot)", &slot0))
|
||||
return 0;
|
||||
if (IsStackable(slot0))
|
||||
Game::Lua::PushNumber(L, 1.0);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_C_IsStackableAction(void *L) {
|
||||
int slot0;
|
||||
if (!SlotArg(L, "Usage: C_ActionBar.IsStackableAction(actionID)", &slot0))
|
||||
return 0;
|
||||
Game::Lua::PushBool(L, IsStackable(slot0));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetItemActionOnEquipSpellID(void *L) {
|
||||
int slot0;
|
||||
if (!SlotArg(L, "Usage: C_ActionBar.GetItemActionOnEquipSpellID(actionID)", &slot0))
|
||||
return 0;
|
||||
const uint8_t *record = ::Item::PeekRecord(ItemForSlot(slot0));
|
||||
if (record == nullptr)
|
||||
return 0;
|
||||
for (int i = 0; i < Offsets::ITEMSTATS_SPELL_SLOT_COUNT; ++i) {
|
||||
const uint32_t spellID = Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_SPELL_ID + i * 4);
|
||||
const uint32_t trigger = Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_SPELL_TRIGGER + i * 4);
|
||||
if (spellID != 0 && trigger == Offsets::ITEM_SPELLTRIGGER_ON_EQUIP) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(spellID));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// --- Documentation ----------------------------------------------------------
|
||||
|
||||
const Game::Doc::Field kSlotArgs[] = {
|
||||
Game::Doc::Req("slot", "number", "The action slot, 1 to 120."),
|
||||
};
|
||||
const Game::Doc::Field kIsStackableRets[] = {
|
||||
Game::Doc::Opt("isStackable", "number", nullptr,
|
||||
"1 if the slot's item stacks, else nil."),
|
||||
};
|
||||
const Game::Doc::Function kIsStackableAction{
|
||||
"Returns 1 if an action slot holds an item that stacks.",
|
||||
kSlotArgs, kIsStackableRets, "ActionBar"};
|
||||
|
||||
const Game::Doc::Field kActionIDArgs[] = {
|
||||
Game::Doc::Req("actionID", "luaIndex"),
|
||||
};
|
||||
const Game::Doc::Field kCIsStackableRets[] = {
|
||||
Game::Doc::Req("isStackableAction", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kCIsStackableAction{
|
||||
"Whether the action's item stacks.",
|
||||
kActionIDArgs, kCIsStackableRets};
|
||||
|
||||
const Game::Doc::Field kOnEquipRets[] = {
|
||||
Game::Doc::Opt("onEquipSpellID", "number", nullptr,
|
||||
"Nil when the action has no item or its item has no on-equip spell."),
|
||||
};
|
||||
const Game::Doc::Function kGetItemActionOnEquipSpellID{
|
||||
"The spell the action's item grants while equipped.",
|
||||
kActionIDArgs, kOnEquipRets};
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("IsStackableAction", &Script_IsStackableAction,
|
||||
&kIsStackableAction);
|
||||
Game::Lua::RegisterTableFunction("C_ActionBar", "IsStackableAction",
|
||||
&Script_C_IsStackableAction, &kCIsStackableAction);
|
||||
Game::Lua::RegisterTableFunction("C_ActionBar", "GetItemActionOnEquipSpellID",
|
||||
&Script_GetItemActionOnEquipSpellID,
|
||||
&kGetItemActionOnEquipSpellID);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace Action::SlotItem
|
||||
@@ -0,0 +1,125 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under
|
||||
// the terms of the GNU General Public License as published by the Free Software
|
||||
// Foundation, either version 3 of the License, or (at your option) any later
|
||||
// version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
// A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `ACTION_USABLE_CHANGED(changes)` — the slots whose usable / noMana pair
|
||||
// changed, as `{ slot, usable, noMana }` entries.
|
||||
//
|
||||
// The engine's bulk refresh FUN_ACTION_UPDATE_USABLE already finds them: it
|
||||
// computes each slot with FUN_ACTION_SLOT_USABLE, compares the pair against
|
||||
// the caches IsUsableAction reads, stores a changed pair, and signals the
|
||||
// argument-less ACTIONBAR_UPDATE_USABLE once when any changed. Two of its
|
||||
// calls are redirected. The per-slot call records the slots whose pair differs
|
||||
// from the caches, the same compare the engine makes next. The signal sends
|
||||
// ACTIONBAR_UPDATE_USABLE unchanged, then this event with the recorded slots.
|
||||
//
|
||||
// The per-slot refresh in FUN_ACTION_SLOT_CHANGED_NOTIFY stores its pair
|
||||
// without this event; it signals ACTIONBAR_SLOT_CHANGED for the slot instead.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "event/Custom.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace Action::UsableChanged {
|
||||
|
||||
namespace {
|
||||
|
||||
using SlotUsable_t = uint32_t(__fastcall *)(uint32_t slot0, uint32_t *outNoMana);
|
||||
using FireEvent_t = void(__fastcall *)(int eventID);
|
||||
|
||||
struct Change {
|
||||
uint32_t slot0;
|
||||
bool usable;
|
||||
bool noMana;
|
||||
};
|
||||
|
||||
// This refresh's changes. The loop runs slots 0..119 in order, so slot 0
|
||||
// starts a new list.
|
||||
Change g_changes[Offsets::ACTION_TABLE_MAX_SLOTS];
|
||||
int g_count = 0;
|
||||
|
||||
const Game::Doc::Field kStateFields[] = {
|
||||
Game::Doc::Req("slot", "luaIndex"),
|
||||
Game::Doc::Req("usable", "bool"),
|
||||
Game::Doc::Req("noMana", "bool"),
|
||||
};
|
||||
const Game::Doc::Structure kState{"ActionUsableState", "ActionBar", kStateFields};
|
||||
|
||||
const Game::Doc::Field kPayload[] = {
|
||||
Game::Doc::Req("changes", "table", "An ActionUsableState for each slot that changed."),
|
||||
};
|
||||
const Game::Doc::Event kDoc{
|
||||
"ActionBar", "Fires when the usable or noMana state of one or more action slots changes.",
|
||||
kPayload};
|
||||
const Event::Custom::AutoReserve kEvent{"ACTION_USABLE_CHANGED", &kDoc};
|
||||
|
||||
uint32_t __fastcall SlotUsable_h(uint32_t slot0, uint32_t *outNoMana) {
|
||||
const uint32_t usable =
|
||||
reinterpret_cast<SlotUsable_t>(Offsets::FUN_ACTION_SLOT_USABLE)(slot0, outNoMana);
|
||||
if (slot0 == 0)
|
||||
g_count = 0;
|
||||
const auto *usableCache =
|
||||
reinterpret_cast<const uint32_t *>(static_cast<uintptr_t>(Offsets::VAR_ACTION_USABLE));
|
||||
const auto *noManaCache =
|
||||
reinterpret_cast<const uint32_t *>(static_cast<uintptr_t>(Offsets::VAR_ACTION_NO_MANA));
|
||||
if (slot0 < static_cast<uint32_t>(Offsets::ACTION_TABLE_MAX_SLOTS) &&
|
||||
g_count < Offsets::ACTION_TABLE_MAX_SLOTS &&
|
||||
(usable != usableCache[slot0] || *outNoMana != noManaCache[slot0]))
|
||||
g_changes[g_count++] = {slot0, usable != 0, *outNoMana != 0};
|
||||
return usable;
|
||||
}
|
||||
|
||||
void __fastcall FireUsable_h(int eventID) {
|
||||
// Copied first: the handlers below can run another refresh.
|
||||
Change changes[Offsets::ACTION_TABLE_MAX_SLOTS];
|
||||
const int count = g_count;
|
||||
std::memcpy(changes, g_changes, sizeof(Change) * count);
|
||||
g_count = 0;
|
||||
|
||||
reinterpret_cast<FireEvent_t>(Offsets::FUN_FIRE_EVENT_NO_ARGS)(eventID);
|
||||
|
||||
const int event = kEvent.Slot();
|
||||
void *L = Game::Lua::State();
|
||||
if (count == 0 || !Event::Custom::HasListeners(event) || L == nullptr)
|
||||
return;
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
Game::Lua::NewTable(L);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldNumber(L, "slot", static_cast<double>(changes[i].slot0 + 1));
|
||||
Game::Lua::SetFieldBool(L, "usable", changes[i].usable);
|
||||
Game::Lua::SetFieldBool(L, "noMana", changes[i].noMana);
|
||||
Game::Lua::RawSet(L, -3);
|
||||
}
|
||||
Game::Lua::SetN(L, -1, count);
|
||||
Event::Custom::FireValues(event, L, top + 1, 1);
|
||||
Game::Lua::SetTop(L, top);
|
||||
}
|
||||
|
||||
void Register() {
|
||||
Common::RedirectCall(Offsets::CALL_UPDATE_USABLE_SLOT_USABLE, Offsets::FUN_ACTION_SLOT_USABLE,
|
||||
reinterpret_cast<const void *>(&SlotUsable_h));
|
||||
Common::RedirectCall(Offsets::CALL_UPDATE_USABLE_FIRE, Offsets::FUN_FIRE_EVENT_NO_ARGS,
|
||||
reinterpret_cast<const void *>(&FireUsable_h));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Register};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Action::UsableChanged
|
||||
+68
-68
@@ -15,18 +15,19 @@
|
||||
// this module makes the engine think they do — without writing anything
|
||||
// to the filesystem. The addons are `Manifest::kAddons` (the `ADDONS`
|
||||
// list in CMakeLists.txt): `!!!ClassicAPI`, which loads at login, and
|
||||
// `ClassicAPI_DebugTools`, which is `## LoadOnDemand` behind the
|
||||
// `/dump` / `/fstack` / `/etrace` stubs in `!!!ClassicAPI` — retail's
|
||||
// chat-frame + Blizzard_DebugTools split. (The `Blizzard_` name is not
|
||||
// `ClassicAPI_DebugTools` and `ClassicAPI_EventTrace`, which are
|
||||
// `## LoadOnDemand` behind the `/dump` / `/fstack` / `/etrace` stubs in
|
||||
// `!!!ClassicAPI` — retail's chat-frame + Blizzard_DebugTools /
|
||||
// Blizzard_EventTrace split. (The `Blizzard_` name is not
|
||||
// available to us: the engine moves loose `Interface\AddOns\Blizzard*`
|
||||
// folders aside at startup, `MoveAsideLooseInterface`, and refuses a
|
||||
// `Blizzard_*` TOC without a signature.)
|
||||
//
|
||||
// How:
|
||||
//
|
||||
// 1. The CMake build embeds every file of every listed addon as an
|
||||
// RCDATA resource, plus a generated `{path, resourceId}` manifest
|
||||
// (`embedded_resources.h`).
|
||||
// 1. The CMake build embeds each listed addon as one RCDATA resource,
|
||||
// a gzipped pack of its files, plus a generated
|
||||
// `{path, resourceId, index}` manifest (`embedded_resources.h`).
|
||||
//
|
||||
// 2. We hook the engine's file-read function at `FUN_FILE_READ`.
|
||||
// When the engine asks for a path under
|
||||
@@ -34,6 +35,9 @@
|
||||
// via the same allocator the engine itself uses, copy the
|
||||
// embedded content in, and hand it back. The caller's normal
|
||||
// `SMemFree` reclaims it on completion — no special lifetime.
|
||||
// A TOC then goes through `AddOns::TocRewrite`, as an installed
|
||||
// addon's does, so its file lines can carry `[AllowLoadGameType ...]`
|
||||
// / `[ExcludeLoadGameType ...]` conditions.
|
||||
//
|
||||
// 3. We hook the exists-check `FUN_FILE_EXISTS` so it answers the same
|
||||
// way for the same paths. Without it every embedded file reads fine
|
||||
@@ -61,6 +65,7 @@
|
||||
|
||||
#include "embedded_resources.h"
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "addons/Embedded.h"
|
||||
@@ -72,6 +77,7 @@
|
||||
#include "addons/TocRewrite.h"
|
||||
#include "bindings/Inject.h"
|
||||
#include "embed/Resource.h"
|
||||
#include "storm/Memory.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
@@ -99,26 +105,12 @@ constexpr const char *kDevMarkerFile = ".classicapi-dev";
|
||||
// paths are case-insensitive.
|
||||
constexpr const char *kAddOnsPrefix = "Interface\\AddOns\\";
|
||||
|
||||
char NormalizeChar(char c) {
|
||||
if (c == '/') return '\\';
|
||||
if (c >= 'A' && c <= 'Z') return static_cast<char>(c + 32);
|
||||
return c;
|
||||
}
|
||||
|
||||
bool PathEqualsCI(const char *a, const char *b) {
|
||||
while (*a && *b) {
|
||||
if (NormalizeChar(*a) != NormalizeChar(*b)) return false;
|
||||
++a; ++b;
|
||||
}
|
||||
return *a == *b;
|
||||
}
|
||||
|
||||
// `path` with the case-insensitive `prefix` stripped, NULL when it does
|
||||
// not start with it.
|
||||
const char *StripPrefixCI(const char *path, const char *prefix) {
|
||||
while (*prefix) {
|
||||
if (*path == '\0') return nullptr;
|
||||
if (NormalizeChar(*path) != NormalizeChar(*prefix)) return nullptr;
|
||||
if (Common::FoldPathChar(*path) != Common::FoldPathChar(*prefix)) return nullptr;
|
||||
++path; ++prefix;
|
||||
}
|
||||
return path;
|
||||
@@ -126,7 +118,7 @@ const char *StripPrefixCI(const char *path, const char *prefix) {
|
||||
|
||||
int IndexOfAddon(const char *name) {
|
||||
for (size_t i = 0; i < kAddonCount; ++i) {
|
||||
if (PathEqualsCI(name, kAddons[i]))
|
||||
if (Common::SamePath(name, kAddons[i]))
|
||||
return static_cast<int>(i);
|
||||
}
|
||||
return -1;
|
||||
@@ -140,7 +132,7 @@ int MatchAddonPath(const char *path, const char **relPath) {
|
||||
if (rel == nullptr) return -1;
|
||||
for (size_t i = 0; i < kAddonCount; ++i) {
|
||||
const char *rest = StripPrefixCI(rel, kAddons[i]);
|
||||
if (rest != nullptr && NormalizeChar(*rest) == '\\') {
|
||||
if (rest != nullptr && Common::FoldPathChar(*rest) == '\\') {
|
||||
*relPath = rel;
|
||||
return static_cast<int>(i);
|
||||
}
|
||||
@@ -151,11 +143,10 @@ int MatchAddonPath(const char *path, const char **relPath) {
|
||||
// The embedded bytes for manifest path `relPath`; `data` is NULL when it
|
||||
// is not embedded.
|
||||
Embed::Bytes LookupEmbedded(const char *relPath) {
|
||||
for (size_t i = 0; i < Manifest::kFileCount; ++i) {
|
||||
if (PathEqualsCI(relPath, Manifest::kFiles[i].path))
|
||||
return Embed::Resource(Manifest::kFiles[i].resourceId);
|
||||
}
|
||||
return {nullptr, 0};
|
||||
const Manifest::File *file = Embed::FindFile(Manifest::kFiles, relPath);
|
||||
if (file == nullptr)
|
||||
return {nullptr, 0};
|
||||
return Embed::File(file->resourceId, file->index);
|
||||
}
|
||||
|
||||
// Engine file I/O + Storm allocator — see addons/EngineIO.h for the shapes and
|
||||
@@ -163,8 +154,6 @@ Embed::Bytes LookupEmbedded(const char *relPath) {
|
||||
// by the caller's standard SMemFree in turn.
|
||||
using AddOns::EngineIO::FileExistsFn;
|
||||
using AddOns::EngineIO::FileReadFn;
|
||||
using AddOns::EngineIO::SMemAllocFn;
|
||||
using AddOns::EngineIO::SMemFreeFn;
|
||||
|
||||
// The trampolines to the original FUN_FILE_READ / FUN_FILE_EXISTS (installed
|
||||
// by the co-hooks below).
|
||||
@@ -287,8 +276,7 @@ void DecideSource(int addon) {
|
||||
// Free the disk buffer we just borrowed — we only needed it for
|
||||
// the version-line scrape. The actual content for the engine's
|
||||
// TOC read comes through the regular hook path below.
|
||||
auto SMemFree = reinterpret_cast<SMemFreeFn>(Offsets::FUN_STORM_SMEM_FREE);
|
||||
SMemFree(diskBuf, __FILE__, __LINE__, 0);
|
||||
Storm::Free(diskBuf, __FILE__, __LINE__);
|
||||
|
||||
// Missing or unparseable disk version → assume it's older than
|
||||
// anything we ship, embedded wins.
|
||||
@@ -301,6 +289,47 @@ void DecideSource(int addon) {
|
||||
g_source[addon] = (cmp > 0) ? Source::Embedded : Source::Disk;
|
||||
}
|
||||
|
||||
// Serves one file of embedded addon `addon` from whichever source won
|
||||
// DecideSource.
|
||||
int ReadAddonFile(int addon, const char *relPath, int unused, const char *path,
|
||||
void **outBuf, size_t *outSize, size_t extraBytes, int flag1,
|
||||
int flag2) {
|
||||
DecideSource(addon);
|
||||
|
||||
if (g_source[addon] == Source::Disk) {
|
||||
// Disk version is at least as new — serve disk. If disk
|
||||
// doesn't have this specific file (e.g. embedded has files
|
||||
// disk doesn't), fall through and try embedded.
|
||||
const int diskResult = FileRead_o(unused, path, outBuf, outSize,
|
||||
extraBytes, flag1, flag2);
|
||||
if (diskResult != 0) return diskResult;
|
||||
}
|
||||
|
||||
// Source::Embedded path, OR Source::Disk-but-file-missing fallback.
|
||||
// Look up the embedded version and synthesize a Storm buffer the
|
||||
// caller can free normally.
|
||||
const Embed::Bytes entry = LookupEmbedded(relPath);
|
||||
if (entry.data == nullptr) {
|
||||
// Not in our embedded set either — let the engine try disk
|
||||
// one more time (returns 0 if that also fails). Covers
|
||||
// pathological cases like an empty disk install + embedded
|
||||
// missing some file.
|
||||
return FileRead_o(unused, path, outBuf, outSize, extraBytes,
|
||||
flag1, flag2);
|
||||
}
|
||||
|
||||
const size_t totalSize = entry.size + extraBytes;
|
||||
void *buf = Storm::Alloc(totalSize, __FILE__, __LINE__);
|
||||
if (buf == nullptr) return 0;
|
||||
std::memcpy(buf, entry.data, entry.size);
|
||||
if (extraBytes > 0) {
|
||||
std::memset(static_cast<uint8_t *>(buf) + entry.size, 0, extraBytes);
|
||||
}
|
||||
if (outBuf != nullptr) *outBuf = buf;
|
||||
if (outSize != nullptr) *outSize = entry.size;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __stdcall FileRead_h(int unused, const char *path, void **outBuf,
|
||||
size_t *outSize, size_t extraBytes,
|
||||
int flag1, int flag2) {
|
||||
@@ -344,42 +373,13 @@ int __stdcall FileRead_h(int unused, const char *path, void **outBuf,
|
||||
return result;
|
||||
}
|
||||
|
||||
DecideSource(addon);
|
||||
|
||||
if (g_source[addon] == Source::Disk) {
|
||||
// Disk version is at least as new — serve disk. If disk
|
||||
// doesn't have this specific file (e.g. embedded has files
|
||||
// disk doesn't), fall through and try embedded.
|
||||
const int diskResult = FileRead_o(unused, path, outBuf, outSize,
|
||||
extraBytes, flag1, flag2);
|
||||
if (diskResult != 0) return diskResult;
|
||||
}
|
||||
|
||||
// Source::Embedded path, OR Source::Disk-but-file-missing fallback.
|
||||
// Look up the embedded version and synthesize a Storm buffer the
|
||||
// caller can free normally.
|
||||
const Embed::Bytes entry = LookupEmbedded(relPath);
|
||||
if (entry.data == nullptr) {
|
||||
// Not in our embedded set either — let the engine try disk
|
||||
// one more time (returns 0 if that also fails). Covers
|
||||
// pathological cases like an empty disk install + embedded
|
||||
// missing some file.
|
||||
return FileRead_o(unused, path, outBuf, outSize, extraBytes,
|
||||
flag1, flag2);
|
||||
}
|
||||
|
||||
auto SMemAlloc = reinterpret_cast<SMemAllocFn>(
|
||||
Offsets::FUN_STORM_SMEM_ALLOC);
|
||||
const size_t totalSize = entry.size + extraBytes;
|
||||
void *buf = SMemAlloc(totalSize, __FILE__, __LINE__, 0);
|
||||
if (buf == nullptr) return 0;
|
||||
std::memcpy(buf, entry.data, entry.size);
|
||||
if (extraBytes > 0) {
|
||||
std::memset(static_cast<uint8_t *>(buf) + entry.size, 0, extraBytes);
|
||||
}
|
||||
if (outBuf != nullptr) *outBuf = buf;
|
||||
if (outSize != nullptr) *outSize = entry.size;
|
||||
return 1;
|
||||
// An embedded addon's TOC takes the same per-line directives as an
|
||||
// installed addon's, whichever source serves it.
|
||||
const int result = ReadAddonFile(addon, relPath, unused, path, outBuf, outSize,
|
||||
extraBytes, flag1, flag2);
|
||||
if (result != 0)
|
||||
AddOns::TocRewrite::Transform(path, outBuf, outSize);
|
||||
return result;
|
||||
}
|
||||
|
||||
// The exists-check counterpart of FileRead_h: the same source decision and
|
||||
|
||||
@@ -32,10 +32,6 @@ using FileReadFn = int(__stdcall *)(int unused, const char *path, void **outBuf,
|
||||
// FUN_FILE_EXISTS (0x00648A30) — __stdcall, RET 8.
|
||||
using FileExistsFn = int(__stdcall *)(const char *path, int mode);
|
||||
|
||||
// FUN_STORM_SMEM_ALLOC / FUN_STORM_SMEM_FREE — __stdcall, RET 0x10. Buffers
|
||||
// FUN_FILE_READ hands out are freed with SMemFreeFn; a buffer we allocate with
|
||||
// SMemAllocFn is freed cleanly by the engine's own SMemFree in turn.
|
||||
using SMemAllocFn = void *(__stdcall *)(size_t size, const char *file, int line, int flags);
|
||||
using SMemFreeFn = void(__stdcall *)(void *buf, const char *file, int line, int flags);
|
||||
// Buffers FUN_FILE_READ hands out are freed with Storm::Free (storm/Memory.h).
|
||||
|
||||
} // namespace AddOns::EngineIO
|
||||
|
||||
+1
-5
@@ -408,11 +408,7 @@ static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_AddOns", "GetAddOnInterfaceVersion",
|
||||
&Script_GetAddOnInterfaceVersion,
|
||||
&kGetAddOnInterfaceVersion);
|
||||
Game::Lua::RegisterIntegerEnum(
|
||||
"Enum", "AddOnSecurityStatus",
|
||||
kAddOnSecurityStatusEntries,
|
||||
sizeof(kAddOnSecurityStatusEntries) /
|
||||
sizeof(kAddOnSecurityStatusEntries[0]));
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "AddOnSecurityStatus", kAddOnSecurityStatusEntries);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#include "addons/EngineIO.h"
|
||||
#include "addons/Toc.h"
|
||||
#include "luasyntax/AddonNamespace.h"
|
||||
#include "storm/Memory.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -51,7 +52,6 @@ using IsLoadedFn_t = uint8_t(__fastcall *)(const char *name);
|
||||
// FUN_FILE_READ address routes through Embedded's file-read hook, which forwards
|
||||
// every non-`!!!ClassicAPI` path to the original untouched — what we want here.
|
||||
using AddOns::EngineIO::FileReadFn;
|
||||
using AddOns::EngineIO::SMemFreeFn;
|
||||
|
||||
// True iff the addon's `.toc` has a line `## AllowAddOnTableAccess: <nonzero>`.
|
||||
// The value is read as a TOC boolean flag: parse the leading integer, true iff
|
||||
@@ -87,8 +87,7 @@ bool TocAllowsTableAccess(const char *name) {
|
||||
allowed = anyDigit && value != 0;
|
||||
}
|
||||
|
||||
auto smemFree = reinterpret_cast<SMemFreeFn>(Offsets::FUN_STORM_SMEM_FREE);
|
||||
smemFree(buf, __FILE__, __LINE__, 0);
|
||||
Storm::Free(buf, __FILE__, __LINE__);
|
||||
return allowed;
|
||||
}
|
||||
|
||||
|
||||
@@ -89,8 +89,8 @@ void PatchLoadWindow() {
|
||||
reinterpret_cast<const void *>(&LoadWindowCloseThunk));
|
||||
}
|
||||
|
||||
int __fastcall Script_UpdateAddOnMemoryUsage(void * /*L*/) {
|
||||
Gc::Tag::UpdateUsage();
|
||||
int __fastcall Script_UpdateAddOnMemoryUsage(void *L) {
|
||||
Gc::Tag::UpdateUsage(L);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -107,30 +107,15 @@ int __fastcall Script_GetAddOnMemoryUsage(void *L) {
|
||||
}
|
||||
|
||||
// `_classicapi_LuaHeapAudit()` → objectBytes, nblocks, taggedObjects.
|
||||
// Walks every collectable object the engine can reach — the main list,
|
||||
// the userdata list and the string table, the three lists `luaC_sweep`
|
||||
// sweeps — summing `Gc::Tag::SizeOf`. `objectBytes` should sit just under
|
||||
// `nblocks` (the engine's own byte count): the gap is what is not a GC
|
||||
// object (string-table buckets, the main thread and its stack, buffers).
|
||||
// A gap that grows with the heap means a size formula is wrong.
|
||||
uint64_t SumList(const uint8_t *object) {
|
||||
uint64_t total = 0;
|
||||
while (object != nullptr) {
|
||||
total += Gc::Tag::SizeOf(object);
|
||||
object = Game::Read<const uint8_t *>(object, Offsets::OFF_LUA_GCHEADER_NEXT);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
// Sums `Gc::Tag::SizeOf` over every collectable object the engine can
|
||||
// reach. `objectBytes` should sit just under `nblocks` (the engine's own
|
||||
// byte count): the gap is what is not a GC object (string-table buckets,
|
||||
// the main thread and its stack, buffers). A gap that grows with the heap
|
||||
// means a size formula is wrong.
|
||||
int __fastcall Script_LuaHeapAudit(void *L) {
|
||||
uint64_t bytes = 0;
|
||||
Gc::Tag::ForEachObject(L, [&bytes](const void *object) { bytes += Gc::Tag::SizeOf(object); });
|
||||
const uint8_t *g = Game::Read<const uint8_t *>(L, Offsets::OFF_LUA_STATE_G);
|
||||
uint64_t bytes = SumList(Game::Read<const uint8_t *>(g, Offsets::OFF_LUA_G_ROOTGC)) +
|
||||
SumList(Game::Read<const uint8_t *>(g, Offsets::OFF_LUA_G_ROOTUDATA));
|
||||
const auto buckets =
|
||||
Game::Read<const uint8_t *const *>(g, Offsets::OFF_LUA_G_STRT_HASH);
|
||||
const int size = Game::Read<int>(g, Offsets::OFF_LUA_G_STRT_SIZE);
|
||||
for (int i = 0; i < size; ++i)
|
||||
bytes += SumList(buckets[i]);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(bytes));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(Game::Read<uint32_t>(g, Offsets::OFF_LUA_G_NBLOCKS)));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(Gc::Tag::TaggedCount()));
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
#include "addons/Embedded.h"
|
||||
#include "addons/EngineIO.h"
|
||||
#include "addons/Registry.h"
|
||||
#include "storm/Memory.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
@@ -235,9 +236,7 @@ bool TryHashToc(const char *name, uint32_t *outHash) {
|
||||
if (!ReadToc(name, &buf, &size))
|
||||
return false;
|
||||
*outHash = HashTocMetadata(static_cast<const char *>(buf), size);
|
||||
reinterpret_cast<AddOns::EngineIO::SMemFreeFn>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_STORM_SMEM_FREE))(
|
||||
buf, __FILE__, __LINE__, 0);
|
||||
Storm::Free(buf, __FILE__, __LINE__);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
#include "addons/EngineIO.h"
|
||||
#include "addons/Toc.h"
|
||||
#include "script/LoadFile.h"
|
||||
#include "storm/Memory.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -66,7 +67,6 @@ constexpr size_t NPOS = static_cast<size_t>(-1);
|
||||
// FlavorToc applies), the exists-check, and the Storm free. See addons/EngineIO.h.
|
||||
using AddOns::EngineIO::FileExistsFn;
|
||||
using AddOns::EngineIO::FileReadFn;
|
||||
using AddOns::EngineIO::SMemFreeFn;
|
||||
using LoadTocFilesFn = uint32_t(__fastcall *)(char *tocPath, int *a2, int *a3);
|
||||
using NameFn = const char *(*)(); // no-arg readers (return in EAX)
|
||||
|
||||
@@ -160,8 +160,7 @@ SvFlags ReadSvFlags(const char *tocPath) {
|
||||
f.perChar =
|
||||
AddOns::Toc::FindValue(b, size, "SavedVariablesPerCharacter", &v, &n) && n > 0;
|
||||
|
||||
reinterpret_cast<SMemFreeFn>(static_cast<uintptr_t>(Offsets::FUN_STORM_SMEM_FREE))(
|
||||
buf, __FILE__, __LINE__, 0);
|
||||
Storm::Free(buf, __FILE__, __LINE__);
|
||||
return f;
|
||||
}
|
||||
|
||||
|
||||
+19
-20
@@ -14,8 +14,11 @@
|
||||
#include "addons/EngineIO.h"
|
||||
#include "addons/TocRewrite.h"
|
||||
#include "addons/Toc.h"
|
||||
#include "localization/Locale.h"
|
||||
|
||||
#include "Offsets.h"
|
||||
#include "storm/Memory.h"
|
||||
#include "turtle/Detect.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -26,24 +29,12 @@ namespace AddOns::TocRewrite {
|
||||
|
||||
namespace {
|
||||
|
||||
using AddOns::EngineIO::SMemAllocFn;
|
||||
using AddOns::EngineIO::SMemFreeFn;
|
||||
using AddOns::Toc::EqCI;
|
||||
using AddOns::Toc::Lower;
|
||||
|
||||
bool IsSpace(char c) { return c == ' ' || c == '\t'; }
|
||||
|
||||
// The client locale code ("enUS", "frFR", …) — the exact string
|
||||
// GetLocale() returns (Offsets.h VAR_LOCALE_NAME_TABLE derivation).
|
||||
const char *ClientLocale() {
|
||||
uint32_t idx = *reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_LOCALE_INDEX));
|
||||
if (idx > 7) idx = 0; // slot 8 overreads the string pool — clamp
|
||||
const char *const *table = reinterpret_cast<const char *const *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_LOCALE_NAME_TABLE));
|
||||
const char *s = table[idx];
|
||||
return (s != nullptr) ? s : "enUS";
|
||||
}
|
||||
using Localization::Locale::Client;
|
||||
|
||||
// True iff the comma-separated `list` (spaces allowed) contains `token` (CI).
|
||||
bool ListContains(const char *list, size_t listLen, const char *token) {
|
||||
@@ -65,7 +56,7 @@ bool ListContains(const char *list, size_t listLen, const char *token) {
|
||||
const char *VariableValue(const char *head, size_t headLen) {
|
||||
if (EqCI(head, headLen, "Family")) return "Classic";
|
||||
if (EqCI(head, headLen, "Game")) return "Vanilla";
|
||||
if (EqCI(head, headLen, "TextLocale")) return ClientLocale();
|
||||
if (EqCI(head, headLen, "TextLocale")) return Client();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -139,15 +130,25 @@ bool TryRewriteInterfaceLine(const char *line, size_t len, std::string &out) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// True iff the game-type list names a game type this client is: always
|
||||
// `vanilla`, and `turtle` (a ClassicAPI extension, like the `_Turtle.toc`
|
||||
// flavor) on a Turtle client.
|
||||
bool ListContainsGameType(const char *list, size_t listLen) {
|
||||
return ListContains(list, listLen, "vanilla") ||
|
||||
(Turtle::Detected() && ListContains(list, listLen, "turtle"));
|
||||
}
|
||||
|
||||
// Evaluate a `[keyword args]` condition — the return is whether the line should
|
||||
// load. An unrecognized keyword returns false so the caller drops the line (a
|
||||
// condition we cannot confirm must not load — fail safe).
|
||||
bool EvalCondition(const char *head, size_t headLen, const char *args,
|
||||
size_t argsLen) {
|
||||
if (EqCI(head, headLen, "AllowLoadGameType"))
|
||||
return ListContains(args, argsLen, "vanilla");
|
||||
return ListContainsGameType(args, argsLen);
|
||||
if (EqCI(head, headLen, "ExcludeLoadGameType"))
|
||||
return !ListContainsGameType(args, argsLen);
|
||||
if (EqCI(head, headLen, "AllowLoadTextLocale"))
|
||||
return ListContains(args, argsLen, ClientLocale());
|
||||
return ListContains(args, argsLen, Client());
|
||||
if (EqCI(head, headLen, "AllowLoad"))
|
||||
return ListContains(args, argsLen, "game"); // addon files: in-game only
|
||||
return false;
|
||||
@@ -286,15 +287,13 @@ void Transform(const char *path, void **outBuf, size_t *outSize) {
|
||||
std::memcmp(rebuilt.data(), content, size) == 0)
|
||||
return; // brackets/commas were only in unaffected lines — nothing changed
|
||||
|
||||
auto SMemAlloc = reinterpret_cast<SMemAllocFn>(Offsets::FUN_STORM_SMEM_ALLOC);
|
||||
auto SMemFree = reinterpret_cast<SMemFreeFn>(Offsets::FUN_STORM_SMEM_FREE);
|
||||
const size_t newLen = rebuilt.size();
|
||||
void *buf = SMemAlloc(newLen + 1, __FILE__, __LINE__, 0); // +1 NUL (extraBytes=1)
|
||||
void *buf = Storm::Alloc(newLen + 1, __FILE__, __LINE__); // +1 NUL (extraBytes=1)
|
||||
if (buf == nullptr) return; // OOM — keep the original
|
||||
std::memcpy(buf, rebuilt.data(), newLen);
|
||||
static_cast<char *>(buf)[newLen] = '\0';
|
||||
|
||||
SMemFree(*outBuf, __FILE__, __LINE__, 0);
|
||||
Storm::Free(*outBuf, __FILE__, __LINE__);
|
||||
*outBuf = buf;
|
||||
if (outSize != nullptr) *outSize = newLen;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,15 @@ namespace AddOns::TocRewrite {
|
||||
// below); metadata-line conditions, added in retail 12.0.7, are out of
|
||||
// scope. What the tokens resolve to on this 1.12 client:
|
||||
//
|
||||
// [AllowLoadGameType ...] keep the line iff the list contains `vanilla`
|
||||
// [AllowLoadGameType ...] keep the line iff the list names a game type
|
||||
// of this client
|
||||
// [ExcludeLoadGameType ...] keep the line iff the list names none
|
||||
// This client is the `vanilla` game type, and
|
||||
// also `turtle` (a ClassicAPI extension, like
|
||||
// the `_Turtle.toc` flavor) when
|
||||
// Turtle::Detected(). So
|
||||
// `[ExcludeLoadGameType turtle]` loads a line
|
||||
// everywhere but Turtle.
|
||||
// [AllowLoadTextLocale ...] keep the line iff the list contains the
|
||||
// client locale (GetLocale() code, e.g. enUS)
|
||||
// [AllowLoad ...] addon files only load in-game, so `game`
|
||||
|
||||
+216
-16
@@ -15,6 +15,17 @@
|
||||
// subclasses (wowdecomp15595 CSimpleAnim.cpp, CSimple*Anim.cpp and their
|
||||
// _Scripts.cpp), on Animation::Object, and the group's CreateAnimation.
|
||||
// Path and its control points are in Path.cpp.
|
||||
//
|
||||
// Later clients' additions, on the same objects:
|
||||
// - the target setters (SetTarget, SetTargetName, SetTargetKey,
|
||||
// SetChildKey, SetTargetParent, GetTarget);
|
||||
// - Alpha's fromAlpha / toAlpha. Blizzard's own code starts a fade from
|
||||
// the region's current alpha with SetFromAlpha(region:GetAlpha()), and
|
||||
// its XML fades in with toAlpha="1" alone, so the alpha is absolute (the
|
||||
// target's alpha becomes from + (to - from) x progress) and an unset
|
||||
// fromAlpha is 0. SetChange goes back to 4.3.4's relative change;
|
||||
// - Scale's SetScaleFrom / SetScaleTo. 4.3.4's scale is the case from = 1,
|
||||
// and SetScale sets the end, so its old results do not change.
|
||||
|
||||
#include "animation/Classes.h"
|
||||
#include "animation/Region.h"
|
||||
@@ -25,7 +36,9 @@
|
||||
#include "baselib/Ascii.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace Animation {
|
||||
|
||||
@@ -79,9 +92,15 @@ Anim::~Anim() {
|
||||
|
||||
// ---- Playback --------------------------------------------------------------------
|
||||
|
||||
bool Anim::Play() {
|
||||
if (targetName[0] != '\0')
|
||||
void Anim::ResolveTarget() {
|
||||
if (!targetKey.empty())
|
||||
target = Region::FindRegionByKey(this, targetKey.c_str());
|
||||
else if (targetName[0] != '\0')
|
||||
target = Region::FindRegionByName(targetName);
|
||||
}
|
||||
|
||||
bool Anim::Play() {
|
||||
ResolveTarget();
|
||||
if (playState != 1) {
|
||||
playState = 1;
|
||||
RunScript(A_ONPLAY, 0);
|
||||
@@ -204,14 +223,21 @@ void Anim::Apply(float p) {
|
||||
return;
|
||||
switch (kind) {
|
||||
case KIND_ALPHA:
|
||||
Region::AddAnimAlpha(region, target, static_cast<int16_t>(change * p));
|
||||
if (alphaFromTo)
|
||||
Region::SetAnimAlpha(region, target, static_cast<uint8_t>((fromAlpha + (toAlpha - fromAlpha) * p) * 255.0f));
|
||||
else
|
||||
Region::AddAnimAlpha(region, target, static_cast<int16_t>(change * p));
|
||||
break;
|
||||
case KIND_TRANSLATION:
|
||||
Region::AddAnimTranslation(region, target, offsetX * p, offsetY * p);
|
||||
break;
|
||||
case KIND_SCALE:
|
||||
Region::AddAnimScale(region, target, originPoint, originX, originY, scaleChangeX * p, scaleChangeY * p);
|
||||
case KIND_SCALE: {
|
||||
// AddAnimScale takes the share of the way to the origin, 1 - scale.
|
||||
const float x = scaleFromX + (scaleToX - scaleFromX) * p;
|
||||
const float y = scaleFromY + (scaleToY - scaleFromY) * p;
|
||||
Region::AddAnimScale(region, target, originPoint, originX, originY, 1.0f - x, 1.0f - y);
|
||||
break;
|
||||
}
|
||||
case KIND_ROTATION:
|
||||
Region::AddAnimRotation(region, target, originPoint, originX, originY, radians * p);
|
||||
break;
|
||||
@@ -473,6 +499,101 @@ int __fastcall Script_GetRegionParent(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---- Target (later clients) ----
|
||||
//
|
||||
// An animation with no target animates its group's region. Each setter
|
||||
// replaces the others' target and returns whether it names a region now;
|
||||
// a name or key is looked up again on Play, as 4.3.4 looks up `target`.
|
||||
|
||||
int PushTargetFound(void *L, const Anim *a) {
|
||||
Game::Lua::PushBool(L, a->target != nullptr);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void *RegionArg(void *L, int idx) {
|
||||
if (Game::Lua::Type(L, idx) != Game::Lua::TYPE_TABLE)
|
||||
return nullptr;
|
||||
void *object = Game::Lua::ResolveObject(L, idx);
|
||||
if (object == nullptr || IsOurs(object) || !Game::Lua::IsA(object, Offsets::VAR_REGION_LUA_TYPE_ID))
|
||||
return nullptr;
|
||||
return object;
|
||||
}
|
||||
|
||||
const char *StringArg(void *L, Anim *a, const char *usage) {
|
||||
if (Game::Lua::IsString(L, 2))
|
||||
return Game::Lua::ToString(L, 2);
|
||||
Game::Lua::Error(L, usage, a->DisplayName());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetTarget(void *L) {
|
||||
Anim *a = Self(L);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
void *region = RegionArg(L, 2);
|
||||
if (region == nullptr) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
a->targetName[0] = '\0';
|
||||
a->targetKey.clear();
|
||||
a->target = region;
|
||||
return PushTargetFound(L, a);
|
||||
}
|
||||
int __fastcall Script_SetTargetName(void *L) {
|
||||
Anim *a = Self(L);
|
||||
const char *name = a != nullptr ? StringArg(L, a, "Usage: %s:SetTargetName(name)") : nullptr;
|
||||
if (name == nullptr)
|
||||
return 0;
|
||||
std::snprintf(a->targetName, sizeof(a->targetName), "%s", CreateName(name, a).c_str());
|
||||
a->targetKey.clear();
|
||||
a->target = nullptr;
|
||||
a->ResolveTarget();
|
||||
return PushTargetFound(L, a);
|
||||
}
|
||||
int __fastcall Script_SetTargetKey(void *L) {
|
||||
Anim *a = Self(L);
|
||||
const char *key = a != nullptr ? StringArg(L, a, "Usage: %s:SetTargetKey(key)") : nullptr;
|
||||
if (key == nullptr)
|
||||
return 0;
|
||||
a->targetName[0] = '\0';
|
||||
a->targetKey = key;
|
||||
a->target = nullptr;
|
||||
a->ResolveTarget();
|
||||
return PushTargetFound(L, a);
|
||||
}
|
||||
int __fastcall Script_SetChildKey(void *L) {
|
||||
Anim *a = Self(L);
|
||||
const char *key = a != nullptr ? StringArg(L, a, "Usage: %s:SetChildKey(childKey)") : nullptr;
|
||||
if (key == nullptr)
|
||||
return 0;
|
||||
a->targetName[0] = '\0';
|
||||
a->targetKey = std::string("$parent.$parent.") + key;
|
||||
a->target = nullptr;
|
||||
a->ResolveTarget();
|
||||
return PushTargetFound(L, a);
|
||||
}
|
||||
int __fastcall Script_SetTargetParent(void *L) {
|
||||
Anim *a = Self(L);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
a->targetName[0] = '\0';
|
||||
a->targetKey.clear();
|
||||
a->target = a->GetRegionParent();
|
||||
return PushTargetFound(L, a);
|
||||
}
|
||||
int __fastcall Script_GetTarget(void *L) {
|
||||
Anim *a = Self(L);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
void *region = a->target != nullptr ? a->target : a->GetRegionParent();
|
||||
if (region != nullptr)
|
||||
Game::Lua::PushObject(L, region);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---- Alpha ----
|
||||
|
||||
int __fastcall Script_SetChange(void *L) {
|
||||
@@ -483,6 +604,7 @@ int __fastcall Script_SetChange(void *L) {
|
||||
const float change = static_cast<float>(Game::Lua::ToNumber(L, 2));
|
||||
const float value = change < -1.0f ? -1.0f : (change >= 1.0f ? 1.0f : change);
|
||||
a->change = static_cast<int16_t>(value * 255.0f);
|
||||
a->alphaFromTo = false;
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_GetChange(void *L) {
|
||||
@@ -493,6 +615,39 @@ int __fastcall Script_GetChange(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Later clients' absolute alpha. Setting either end turns it on; the other
|
||||
// end keeps its value, 0 until set.
|
||||
void SetAlphaEnd(void *L, float Anim::*end, const char *usage) {
|
||||
Anim *a = SelfOf(L, Anim::s_alphaClass);
|
||||
if (a == nullptr || !NumberArg(L, 2, a, usage))
|
||||
return;
|
||||
const float value = static_cast<float>(Game::Lua::ToNumber(L, 2));
|
||||
a->*end = value < 0.0f ? 0.0f : (value > 1.0f ? 1.0f : value);
|
||||
a->alphaFromTo = true;
|
||||
}
|
||||
int __fastcall Script_SetFromAlpha(void *L) {
|
||||
SetAlphaEnd(L, &Anim::fromAlpha, "Usage: %s:SetFromAlpha(alpha)");
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_SetToAlpha(void *L) {
|
||||
SetAlphaEnd(L, &Anim::toAlpha, "Usage: %s:SetToAlpha(alpha)");
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_GetFromAlpha(void *L) {
|
||||
Anim *a = SelfOf(L, Anim::s_alphaClass);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, a->fromAlpha);
|
||||
return 1;
|
||||
}
|
||||
int __fastcall Script_GetToAlpha(void *L) {
|
||||
Anim *a = SelfOf(L, Anim::s_alphaClass);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, a->toAlpha);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---- Translation ----
|
||||
|
||||
int __fastcall Script_SetOffset(void *L) {
|
||||
@@ -572,19 +727,57 @@ int __fastcall Script_SetScale(void *L) {
|
||||
Game::Lua::Error(L, "Usage: %s:SetScale(x, y)", a->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
// CSimpleScaleAnim::SetScale.
|
||||
// CSimpleScaleAnim::SetScale: the scale it ends on.
|
||||
const float x = static_cast<float>(Game::Lua::ToNumber(L, 2));
|
||||
const float y = static_cast<float>(Game::Lua::ToNumber(L, 3));
|
||||
a->scaleChangeX = 1.0f - (x >= 0.001f ? x : 0.001f);
|
||||
a->scaleChangeY = 1.0f - (y >= 0.001f ? y : 0.001f);
|
||||
a->scaleToX = x >= 0.001f ? x : 0.001f;
|
||||
a->scaleToY = y >= 0.001f ? y : 0.001f;
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_GetScale(void *L) {
|
||||
Anim *a = SelfOf(L, Anim::s_scaleClass);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, 1.0f - a->scaleChangeX);
|
||||
Game::Lua::PushNumber(L, 1.0f - a->scaleChangeY);
|
||||
Game::Lua::PushNumber(L, a->scaleToX);
|
||||
Game::Lua::PushNumber(L, a->scaleToY);
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Later clients' SetScaleFrom / SetScaleTo: the scale the animation starts
|
||||
// and ends on, as given.
|
||||
void SetScaleEnd(void *L, float Anim::*x, float Anim::*y, const char *usage) {
|
||||
Anim *a = SelfOf(L, Anim::s_scaleClass);
|
||||
if (a == nullptr)
|
||||
return;
|
||||
if (!Game::Lua::IsNumber(L, 2) || !Game::Lua::IsNumber(L, 3)) {
|
||||
Game::Lua::Error(L, usage, a->DisplayName());
|
||||
return;
|
||||
}
|
||||
a->*x = static_cast<float>(Game::Lua::ToNumber(L, 2));
|
||||
a->*y = static_cast<float>(Game::Lua::ToNumber(L, 3));
|
||||
}
|
||||
int __fastcall Script_SetScaleFrom(void *L) {
|
||||
SetScaleEnd(L, &Anim::scaleFromX, &Anim::scaleFromY, "Usage: %s:SetScaleFrom(x, y)");
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_SetScaleTo(void *L) {
|
||||
SetScaleEnd(L, &Anim::scaleToX, &Anim::scaleToY, "Usage: %s:SetScaleTo(x, y)");
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_GetScaleFrom(void *L) {
|
||||
Anim *a = SelfOf(L, Anim::s_scaleClass);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, a->scaleFromX);
|
||||
Game::Lua::PushNumber(L, a->scaleFromY);
|
||||
return 2;
|
||||
}
|
||||
int __fastcall Script_GetScaleTo(void *L) {
|
||||
Anim *a = SelfOf(L, Anim::s_scaleClass);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, a->scaleToX);
|
||||
Game::Lua::PushNumber(L, a->scaleToY);
|
||||
return 2;
|
||||
}
|
||||
|
||||
@@ -647,24 +840,31 @@ const Game::Lua::FrameMethodEntry kAnimMethods[] = {
|
||||
{"GetSmoothing", &Script_GetSmoothing},
|
||||
{"SetParent", &Script_SetParent},
|
||||
{"GetRegionParent", &Script_GetRegionParent},
|
||||
{"SetTarget", &Script_SetTarget},
|
||||
{"SetTargetName", &Script_SetTargetName},
|
||||
{"SetTargetKey", &Script_SetTargetKey},
|
||||
{"SetChildKey", &Script_SetChildKey},
|
||||
{"SetTargetParent", &Script_SetTargetParent},
|
||||
{"GetTarget", &Script_GetTarget},
|
||||
{"HasScript", &Script_HasScript},
|
||||
{"GetScript", &Script_GetScript},
|
||||
{"SetScript", &Script_SetScript},
|
||||
{"HookScript", &Script_HookScript},
|
||||
};
|
||||
const Game::Lua::FrameMethodEntry kAlphaMethods[] = {
|
||||
{"SetChange", &Script_SetChange},
|
||||
{"GetChange", &Script_GetChange},
|
||||
{"SetChange", &Script_SetChange}, {"GetChange", &Script_GetChange},
|
||||
{"SetFromAlpha", &Script_SetFromAlpha}, {"GetFromAlpha", &Script_GetFromAlpha},
|
||||
{"SetToAlpha", &Script_SetToAlpha}, {"GetToAlpha", &Script_GetToAlpha},
|
||||
};
|
||||
const Game::Lua::FrameMethodEntry kTranslationMethods[] = {
|
||||
{"SetOffset", &Script_SetOffset},
|
||||
{"GetOffset", &Script_GetOffset},
|
||||
};
|
||||
const Game::Lua::FrameMethodEntry kScaleMethods[] = {
|
||||
{"SetOrigin", &Script_SetOrigin},
|
||||
{"GetOrigin", &Script_GetOrigin},
|
||||
{"SetScale", &Script_SetScale},
|
||||
{"GetScale", &Script_GetScale},
|
||||
{"SetOrigin", &Script_SetOrigin}, {"GetOrigin", &Script_GetOrigin},
|
||||
{"SetScale", &Script_SetScale}, {"GetScale", &Script_GetScale},
|
||||
{"SetScaleFrom", &Script_SetScaleFrom}, {"GetScaleFrom", &Script_GetScaleFrom},
|
||||
{"SetScaleTo", &Script_SetScaleTo}, {"GetScaleTo", &Script_GetScaleTo},
|
||||
};
|
||||
const Game::Lua::FrameMethodEntry kRotationMethods[] = {
|
||||
{"SetOrigin", &Script_SetOrigin}, {"GetOrigin", &Script_GetOrigin}, {"SetDegrees", &Script_SetDegrees},
|
||||
|
||||
+21
-3
@@ -16,6 +16,7 @@
|
||||
#include "animation/Object.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// 4.3.4's CSimpleAnimGroup and CSimpleAnim (and its Alpha, Translation,
|
||||
@@ -62,6 +63,10 @@ class Group : public Object {
|
||||
bool HasAlphaAnim() const;
|
||||
bool HasPositionAnim() const;
|
||||
|
||||
// Later clients' setToFinalAlpha: when the group finishes, its alpha
|
||||
// animations' targets keep the alpha they end on (KeepAnimAlpha).
|
||||
bool setToFinalAlpha = false;
|
||||
|
||||
void NotifyAnimAdded(Anim *anim);
|
||||
void NotifyOrderChanged(Anim *anim, int oldOrder);
|
||||
void NotifyAnimRemoved(Anim *anim);
|
||||
@@ -141,17 +146,30 @@ class Anim : public Object {
|
||||
uint8_t flags = 0;
|
||||
int playState = 0; // 0 stopped, 1 playing, 2 paused
|
||||
int loopState = STATE_NONE;
|
||||
void *target = nullptr; // m_target, resolved from targetName on Play
|
||||
void *target = nullptr; // m_target, resolved from targetName or targetKey on Play
|
||||
char targetName[128] = {}; // m_targetName
|
||||
// Later clients' targetKey: a key path from the animation itself, where
|
||||
// `$parent` steps to the group, then the group's region, then that
|
||||
// region's parent. childKey "Icon" is "$parent.$parent.Icon".
|
||||
std::string targetKey;
|
||||
bool hasSmoother = false;
|
||||
float smoothIn = 0.0f, smoothOut = 0.0f;
|
||||
|
||||
// Alpha: m_change, -255..255.
|
||||
// Resolves targetName or targetKey into target, when either is set.
|
||||
void ResolveTarget();
|
||||
|
||||
// Alpha: m_change, -255..255, or (alphaFromTo) the absolute fromAlpha /
|
||||
// toAlpha, 0..1, that later clients replaced it with.
|
||||
int16_t change = 0;
|
||||
bool alphaFromTo = false;
|
||||
float fromAlpha = 0.0f, toAlpha = 0.0f;
|
||||
// Translation: the offset; Scale: the scale and origin; Rotation: the
|
||||
// angle and origin. Offsets in layout units, as 4.3.4 keeps them.
|
||||
float offsetX = 0.0f, offsetY = 0.0f;
|
||||
float scaleChangeX = 0.0f, scaleChangeY = 0.0f; // m_scaleChange: 1 - scale
|
||||
// Scale: from and to, 1 by default. 4.3.4's m_scaleChange (1 - scale)
|
||||
// is the case from = 1; later clients added the from scale.
|
||||
float scaleFromX = 1.0f, scaleFromY = 1.0f;
|
||||
float scaleToX = 1.0f, scaleToY = 1.0f;
|
||||
float radians = 0.0f;
|
||||
int originPoint = 4; // FRAMEPOINT_CENTER
|
||||
float originX = 0.0f, originY = 0.0f;
|
||||
|
||||
+24
-1
@@ -12,7 +12,10 @@
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// 4.3.4's CSimpleAnimGroup (wowdecomp15595 CSimpleAnimGroup.cpp and
|
||||
// CSimpleAnimGroup_Scripts.cpp), on Animation::Object.
|
||||
// CSimpleAnimGroup_Scripts.cpp), on Animation::Object, with later clients'
|
||||
// SetToFinalAlpha / IsSetToFinalAlpha: the final alpha is kept when the
|
||||
// group finishes (Finish included), and Stop still restores the alpha. No
|
||||
// source on disk says what a later client's Stop does with the flag.
|
||||
|
||||
#include "animation/Classes.h"
|
||||
#include "animation/Region.h"
|
||||
@@ -343,6 +346,12 @@ bool Group::OnUpdate(float elapsedSec) {
|
||||
RunScript(G_ONUPDATE, 1);
|
||||
}
|
||||
if (done) {
|
||||
// setToFinalAlpha: the alpha each alpha animation's target finished on
|
||||
// becomes what the end restores. Stop restores the alpha as before.
|
||||
if (setToFinalAlpha)
|
||||
for (Anim *anim : animations)
|
||||
if (anim->kind == KIND_ALPHA)
|
||||
Region::KeepAnimAlpha(RegionObject(), anim->target);
|
||||
Region::NotifyEnd(RegionObject(), this);
|
||||
const bool requested = (flags & GROUP_PENDING_FINISH) != 0;
|
||||
loopState = STATE_NONE;
|
||||
@@ -632,6 +641,18 @@ int __fastcall Script_GetIgnoreFramerateThrottle(void *L) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
int __fastcall Script_SetToFinalAlpha(void *L) {
|
||||
if (Group *g = Self(L))
|
||||
g->setToFinalAlpha = Game::Lua::ToFlag(L, 2);
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_IsSetToFinalAlpha(void *L) {
|
||||
Group *g = Self(L);
|
||||
if (g == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushBool(L, g->setToFinalAlpha);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Lua::FrameMethodEntry kMethods[] = {
|
||||
{"GetObjectType", &Script_GetObjectType},
|
||||
@@ -662,6 +683,8 @@ const Game::Lua::FrameMethodEntry kMethods[] = {
|
||||
{"HookScript", &Script_HookScript},
|
||||
{"SetIgnoreFramerateThrottle", &Script_SetIgnoreFramerateThrottle},
|
||||
{"GetIgnoreFramerateThrottle", &Script_GetIgnoreFramerateThrottle},
|
||||
{"SetToFinalAlpha", &Script_SetToFinalAlpha},
|
||||
{"IsSetToFinalAlpha", &Script_IsSetToFinalAlpha},
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -61,13 +61,16 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "frame/Layout.h"
|
||||
#include "frame/Alpha.h"
|
||||
#include "frame/Destruction.h"
|
||||
#include "texture/Transform.h"
|
||||
#include "xml/Anchors.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
@@ -113,7 +116,6 @@ using SetColor_t = void(__fastcall *)(void *region, void *edx, const uint32_t *b
|
||||
using StoreCorners_t = void(__fastcall *)(void *region, void *edx, float *rect);
|
||||
using Crop_t = void(__fastcall *)(void *region, void *edx, float *rect);
|
||||
using SetStringPos_t = void(__fastcall *)(void *block, const float *position);
|
||||
using NdcToDdc_t = void(__fastcall *)(float *outX, float *outY, float x, float y);
|
||||
using SizeChanged_t = void(__fastcall *)(void *layout, void *edx, const float *oldRect);
|
||||
using InvalidateNode_t = void(__fastcall *)(void *node);
|
||||
|
||||
@@ -162,8 +164,20 @@ uint32_t VertexColor(void *region) {
|
||||
return bgra;
|
||||
}
|
||||
|
||||
// An unchanged alpha writes nothing. The setter already does nothing when the
|
||||
// one entry it would store is the region's colour, but the getter reads a
|
||||
// gradient back as white, and the setter shrinks a gradient's four entries to
|
||||
// that one (benilla-re ui/texture, "Setting a vertex colour"). So restoring
|
||||
// the stored alpha (PreOnAnimUpdate, AnimDeactivated) on a gradient under an
|
||||
// ancestor's alpha group flattened it to white. 3.3.5's and 4.3.4's
|
||||
// SetVertexColor drop the gradient there too; here only a region that an
|
||||
// animation itself fades loses it. The ancestor's fade still reaches the
|
||||
// gradient: a frame's alpha change refolds every region's entries (0x77FAC0).
|
||||
void SetVertexAlpha(void *region, uint8_t alpha) {
|
||||
const uint32_t bgra = (VertexColor(region) & 0x00FFFFFF) | (static_cast<uint32_t>(alpha) << 24);
|
||||
const uint32_t color = VertexColor(region);
|
||||
if ((color >> 24) == alpha)
|
||||
return;
|
||||
const uint32_t bgra = (color & 0x00FFFFFF) | (static_cast<uint32_t>(alpha) << 24);
|
||||
reinterpret_cast<SetColor_t>(Offsets::FUN_FONTSTRING_SET_COLOR)(region, nullptr, &bgra);
|
||||
}
|
||||
|
||||
@@ -202,7 +216,7 @@ bool GetStringPos(void *block, float out[3]) {
|
||||
if (node == nullptr)
|
||||
return false;
|
||||
const float *position = Game::Ptr<const float>(node, Offsets::OFF_TEXT_NODE_POSITION);
|
||||
reinterpret_cast<NdcToDdc_t>(Offsets::FUN_COORD_NDC_TO_DDC)(&out[0], &out[1], position[0], position[1]);
|
||||
Frame::Layout::NdcToDdc(&out[0], &out[1], position[0], position[1]);
|
||||
out[2] = position[2];
|
||||
return true;
|
||||
}
|
||||
@@ -546,9 +560,8 @@ void RegisterLuaFunctions() {
|
||||
RegisterClass(Anim::s_rotationClass);
|
||||
RegisterClass(Anim::s_pathClass);
|
||||
RegisterClass(ControlPoint::s_class);
|
||||
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_REGION_METHOD_REGISTRY), kRegionMethods,
|
||||
static_cast<int>(sizeof(kRegionMethods) / sizeof(kRegionMethods[0])),
|
||||
kRegionDocs, static_cast<int>(sizeof(kRegionDocs) / sizeof(kRegionDocs[0])));
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_REGION_METHOD_REGISTRY), kRegionMethods, kRegionDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
@@ -671,6 +684,28 @@ void AddAnimAlpha(void *region, void *target, int16_t alpha) {
|
||||
SetVertexAlpha(region, static_cast<uint8_t>(value > 255 ? 255 : (value < 0 ? 0 : value)));
|
||||
}
|
||||
|
||||
void SetAnimAlpha(void *region, void *target, uint8_t alpha) {
|
||||
if (IsFrame(region)) {
|
||||
if (target != nullptr && target != region) {
|
||||
ForEachChild(region, [target, alpha](void *child) { SetAnimAlpha(child, target, alpha); });
|
||||
return;
|
||||
}
|
||||
if (alpha != OwnAlpha(region))
|
||||
SetOwnAlpha(region, alpha);
|
||||
return;
|
||||
}
|
||||
if (target != nullptr && target != region)
|
||||
return;
|
||||
SetVertexAlpha(region, alpha);
|
||||
}
|
||||
|
||||
void KeepAnimAlpha(void *region, void *target) {
|
||||
void *shown = target != nullptr ? target : region;
|
||||
AnimData *data = DataOf(shown);
|
||||
if (data != nullptr && data->hasAlpha)
|
||||
data->alpha = IsFrame(shown) ? OwnAlpha(shown) : static_cast<uint8_t>(VertexColor(shown) >> 24);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// The corners (TL, BL, TR, BR) as 2D points.
|
||||
@@ -830,4 +865,29 @@ void *FindRegionByName(const char *name) {
|
||||
return object;
|
||||
}
|
||||
|
||||
// The leading `$parent` steps that stay among our objects (an animation to
|
||||
// its group, a group to its region) are taken here; the rest of the path,
|
||||
// from that region, is the one Xml::Anchors resolves for relativeKey.
|
||||
void *FindRegionByKey(Object *from, const char *path) {
|
||||
if (from == nullptr || path == nullptr)
|
||||
return nullptr;
|
||||
void *current = from;
|
||||
const char *p = path;
|
||||
while (current != nullptr && IsOurs(current)) {
|
||||
const char *dot = std::strchr(p, '.');
|
||||
const std::string part = dot != nullptr ? std::string(p, dot) : std::string(p);
|
||||
if (Game::StrCmpI(part.c_str(), "$parent") != 0)
|
||||
return nullptr; // a key on a group or animation names no region
|
||||
current = static_cast<Object *>(current)->parent;
|
||||
p = dot != nullptr ? dot + 1 : p + part.size();
|
||||
}
|
||||
if (current == nullptr)
|
||||
return nullptr;
|
||||
if (*p != '\0')
|
||||
current = ::Xml::Anchors::ResolveKeyPath(current, p, false);
|
||||
if (current == nullptr || IsOurs(current) || !Game::Lua::IsA(current, Offsets::VAR_REGION_LUA_TYPE_ID))
|
||||
return nullptr;
|
||||
return current;
|
||||
}
|
||||
|
||||
} // namespace Animation::Region
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
// frames, textures and font strings, which have no room for them.
|
||||
namespace Animation {
|
||||
class Group;
|
||||
class Object;
|
||||
}
|
||||
|
||||
namespace Animation::Region {
|
||||
@@ -35,6 +36,12 @@ void NotifyEnd(void *region, Group *group);
|
||||
void PreOnAnimUpdate(void *region);
|
||||
// CScriptRegion::AddAnimAlpha(target, alpha) on the group's region.
|
||||
void AddAnimAlpha(void *region, void *target, int16_t alpha);
|
||||
// Later clients' absolute alpha (Alpha fromAlpha / toAlpha): the same target
|
||||
// rule as AddAnimAlpha, but the alpha becomes `alpha` instead of growing.
|
||||
void SetAnimAlpha(void *region, void *target, uint8_t alpha);
|
||||
// AnimationGroup setToFinalAlpha: the alpha that `target` (the group's
|
||||
// `region` when null) shows now becomes the one the group's end restores.
|
||||
void KeepAnimAlpha(void *region, void *target);
|
||||
// CScriptRegion::AddAnimTranslation(target, translation), in layout units:
|
||||
// moves the drawn textures and font strings, never a rect.
|
||||
void AddAnimTranslation(void *region, void *target, float x, float y);
|
||||
@@ -58,5 +65,9 @@ bool StringAnimated(void *fontString);
|
||||
// CScriptObject::GetScriptObjectByName for a region (Animation:Play's
|
||||
// target lookup): the region named `name`, or nullptr.
|
||||
void *FindRegionByName(const char *name);
|
||||
// The region a later-client targetKey path names, read from `from`: each
|
||||
// dot-separated part is `$parent` (the object's parent) or a key on the
|
||||
// current object's table. Nullptr unless the path ends on a region.
|
||||
void *FindRegionByKey(Object *from, const char *path);
|
||||
|
||||
} // namespace Animation::Region
|
||||
|
||||
+56
-2
@@ -16,6 +16,11 @@
|
||||
// LoadXML_AnimOrigin; the attributes BinaryXML.cpp parses), on 1.12 nodes.
|
||||
// A script body compiles through the engine's own SetScriptObjectScript, so
|
||||
// it is named "<object>:<script>" and handled like a frame's.
|
||||
//
|
||||
// Later clients' attributes go to the same fields their methods set:
|
||||
// setToFinalAlpha and parentArray on a group; targetKey and childKey on any animation;
|
||||
// fromAlpha / toAlpha on Alpha; fromScaleX / fromScaleY / toScaleX /
|
||||
// toScaleY on Scale. Each applies only when present and non-empty.
|
||||
|
||||
#include "animation/Xml.h"
|
||||
|
||||
@@ -54,6 +59,8 @@ bool FloatAttr(const void *node, const char *name, float &out) {
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// object[parentKey] = child, as 4.3.4's LoadXML does for both.
|
||||
void BindParentKey(void *object, Object *child, const char *key) {
|
||||
if (key == nullptr || *key == '\0')
|
||||
@@ -70,6 +77,22 @@ void BindParentKey(void *object, Object *child, const char *key) {
|
||||
Game::Lua::SetTop(L, top);
|
||||
}
|
||||
|
||||
// Later clients' parentArray on a group: appended to object[key], as a
|
||||
// frame or region is.
|
||||
void BindParentArray(void *object, Object *child, const char *key) {
|
||||
if (key == nullptr || *key == '\0')
|
||||
return;
|
||||
void *L = Game::Lua::State();
|
||||
if (L == nullptr)
|
||||
return;
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
if (::Xml::PushWrapper(L, object)) {
|
||||
Push(L, child);
|
||||
::Xml::AppendToArray(L, top + 1, key);
|
||||
}
|
||||
Game::Lua::SetTop(L, top);
|
||||
}
|
||||
|
||||
// <Scripts>: each element's body, its `function` global, or its `method`
|
||||
// field on the object.
|
||||
void LoadScripts(Object *object, const void *node) {
|
||||
@@ -124,6 +147,8 @@ void LoadKeyValues(Object *object, const void *node, void *status) {
|
||||
Game::Lua::SetTop(L, top);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// LoadXML_AnimOrigin: <Origin point="..."><Offset x= y=/></Origin>.
|
||||
void LoadOrigin(Anim *anim, const void *node) {
|
||||
const void *origin = FindChild(node, "Origin");
|
||||
@@ -205,6 +230,7 @@ void LoadGroupNode(Group *group, const void *node, void *status) {
|
||||
LoadGroupNode(group, base, status);
|
||||
}
|
||||
BindParentKey(group->RegionObject(), group, ::Xml::Attr(node, "parentKey"));
|
||||
BindParentArray(group->RegionObject(), group, ::Xml::Attr(node, "parentArray"));
|
||||
if (const char *looping = ::Xml::Attr(node, "looping"); looping != nullptr && *looping != '\0') {
|
||||
LoopType type;
|
||||
if (StringToLoopType(looping, type))
|
||||
@@ -212,6 +238,8 @@ void LoadGroupNode(Group *group, const void *node, void *status) {
|
||||
else
|
||||
::Xml::Warn(status, "AnimationGroup %s: Invalid looping value: %s", group->DisplayName(), looping);
|
||||
}
|
||||
if (const char *keep = ::Xml::Attr(node, "setToFinalAlpha"); keep != nullptr && *keep != '\0')
|
||||
group->setToFinalAlpha = Game::StringToFlag(keep);
|
||||
float x = 0.0f, y = 0.0f;
|
||||
FloatAttr(node, "initialOffsetX", x);
|
||||
FloatAttr(node, "initialOffsetY", y);
|
||||
@@ -262,8 +290,13 @@ void LoadAnim(Anim *anim, const void *node, void *status) {
|
||||
if (FloatAttr(node, "scaleY", sy) && sy < 0.001f)
|
||||
::Xml::Warn(status, "%s: Invalid scaleY value: %s. Value must be at least 0.001.", anim->DisplayName(),
|
||||
::Xml::Attr(node, "scaleY"));
|
||||
anim->scaleChangeX = 1.0f - (sx >= 0.001f ? sx : 0.001f);
|
||||
anim->scaleChangeY = 1.0f - (sy >= 0.001f ? sy : 0.001f);
|
||||
anim->scaleToX = sx >= 0.001f ? sx : 0.001f;
|
||||
anim->scaleToY = sy >= 0.001f ? sy : 0.001f;
|
||||
// Later clients' from and to scale, each axis when given.
|
||||
FloatAttr(node, "fromScaleX", anim->scaleFromX);
|
||||
FloatAttr(node, "fromScaleY", anim->scaleFromY);
|
||||
FloatAttr(node, "toScaleX", anim->scaleToX);
|
||||
FloatAttr(node, "toScaleY", anim->scaleToY);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -285,6 +318,16 @@ void LoadAnim(Anim *anim, const void *node, void *status) {
|
||||
if (name.size() + 1 > sizeof(anim->targetName))
|
||||
::Xml::Warn(status, "target key too long: %s", name.c_str());
|
||||
std::snprintf(anim->targetName, sizeof(anim->targetName), "%s", name.c_str());
|
||||
anim->targetKey.clear();
|
||||
}
|
||||
// Later clients' key paths, looked up on Play like `target`.
|
||||
if (const char *key = ::Xml::Attr(node, "targetKey"); key != nullptr && *key != '\0') {
|
||||
anim->targetName[0] = '\0';
|
||||
anim->targetKey = key;
|
||||
}
|
||||
if (const char *key = ::Xml::Attr(node, "childKey"); key != nullptr && *key != '\0') {
|
||||
anim->targetName[0] = '\0';
|
||||
anim->targetKey = std::string("$parent.$parent.") + key;
|
||||
}
|
||||
if (FloatAttr(node, "startDelay", x))
|
||||
anim->startDelay = x < 0.0f ? 0.0f : x;
|
||||
@@ -319,6 +362,17 @@ void LoadAnim(Anim *anim, const void *node, void *status) {
|
||||
x = 0.0f;
|
||||
}
|
||||
anim->change = static_cast<int16_t>((x < -1.0f ? -1.0f : (x >= 1.0f ? 1.0f : x)) * 255.0f);
|
||||
if (::Xml::Attr(node, "change") != nullptr)
|
||||
anim->alphaFromTo = false;
|
||||
// Later clients' absolute alpha: either end turns it on.
|
||||
if (FloatAttr(node, "fromAlpha", x)) {
|
||||
anim->fromAlpha = x < 0.0f ? 0.0f : (x > 1.0f ? 1.0f : x);
|
||||
anim->alphaFromTo = true;
|
||||
}
|
||||
if (FloatAttr(node, "toAlpha", x)) {
|
||||
anim->toAlpha = x < 0.0f ? 0.0f : (x > 1.0f ? 1.0f : x);
|
||||
anim->alphaFromTo = true;
|
||||
}
|
||||
}
|
||||
if (anim->kind == KIND_SCALE || anim->kind == KIND_ROTATION)
|
||||
LoadOrigin(anim, node);
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
// (RegisterXMLTemplate), whatever its tag, so `inherits` resolves through
|
||||
// Xml::Template as 4.3.4's XMLHashNodeRef does.
|
||||
namespace Animation {
|
||||
class Object;
|
||||
class Group;
|
||||
class Anim;
|
||||
class ControlPoint;
|
||||
@@ -39,4 +40,13 @@ void LoadControlPoint(ControlPoint *point, const void *node, void *status);
|
||||
// The template node `name` resolves to, or nullptr.
|
||||
const void *Template(const char *name);
|
||||
|
||||
// What every script object's element shares (Model::Scene's actors too):
|
||||
// object[key] = child for parentKey, an append to object[key] for
|
||||
// parentArray, the <Scripts> (a body, a `function` global or a `method`
|
||||
// field) and the <KeyValues>.
|
||||
void BindParentKey(void *object, Object *child, const char *key);
|
||||
void BindParentArray(void *object, Object *child, const char *key);
|
||||
void LoadScripts(Object *object, const void *node);
|
||||
void LoadKeyValues(Object *object, const void *node, void *status);
|
||||
|
||||
} // namespace Animation::Xml
|
||||
|
||||
+63
-11
@@ -36,8 +36,8 @@
|
||||
// listed by the coverage function
|
||||
// - a frame method -> Blizzard's widget system for its registry
|
||||
// ("SimpleTextureAPI", Type "ScriptObject")
|
||||
// - an enum / structure /
|
||||
// event -> the System named in its declaration
|
||||
// - an enum / constants /
|
||||
// structure / event -> the System named in its declaration
|
||||
// A system's `Environment` is "All" when its functions were recorded in both
|
||||
// Lua states, else "Game" or "Glue"; a function whose own environment is
|
||||
// narrower than its system's gets a Documentation line saying so.
|
||||
@@ -79,13 +79,20 @@ enum Kind : uint8_t {
|
||||
KIND_TABLE, // RegisterTableFunction
|
||||
KIND_FRAME_METHOD, // RegisterFrameMethods, one per entry
|
||||
KIND_ENUM, // RegisterIntegerEnum
|
||||
KIND_CONSTANTS, // RegisterConstants
|
||||
KIND_ORPHAN_METHOD_DOC // a Doc::Method naming no entry in its table
|
||||
};
|
||||
|
||||
// The value-table kinds, which share the enum* fields of Entry.
|
||||
bool IsValueTable(Kind kind) {
|
||||
return kind == KIND_ENUM || kind == KIND_CONSTANTS;
|
||||
}
|
||||
|
||||
struct Entry {
|
||||
Kind kind;
|
||||
unsigned env; // Env bits the registration was recorded in
|
||||
const char *table; // KIND_TABLE: the namespace; KIND_ENUM: the parent ("Enum")
|
||||
const char *table; // KIND_TABLE: the namespace; KIND_ENUM: the parent ("Enum");
|
||||
// KIND_CONSTANTS: "Constants"
|
||||
const char *name; // registered name / method name / enum sub-name
|
||||
const void *registry; // frame methods only
|
||||
const Function *doc; // nullptr = undocumented
|
||||
@@ -195,6 +202,7 @@ const char *SystemOf(const Entry &e) {
|
||||
case KIND_FRAME_METHOD:
|
||||
return WidgetSystemFor(e.registry);
|
||||
case KIND_ENUM:
|
||||
case KIND_CONSTANTS:
|
||||
return e.enumSystem;
|
||||
default:
|
||||
return nullptr;
|
||||
@@ -240,6 +248,7 @@ std::vector<SystemInfo> CollectSystems() {
|
||||
AddSystem(systems, SystemOf(e), nullptr, "ScriptObject", e.env);
|
||||
break;
|
||||
case KIND_ENUM:
|
||||
case KIND_CONSTANTS:
|
||||
AddSystem(systems, e.enumSystem, nullptr, nullptr, 0);
|
||||
break;
|
||||
default:
|
||||
@@ -394,6 +403,25 @@ void PushEnumeration(void *L, const Entry &e) {
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
// Pushes { Name, Type = "Constants",
|
||||
// Values = { { Name, Type = "number", Value } } }.
|
||||
void PushConstants(void *L, const Entry &e) {
|
||||
Lua::NewTable(L);
|
||||
Lua::SetFieldString(L, "Name", e.name);
|
||||
Lua::SetFieldString(L, "Type", "Constants");
|
||||
Lua::PushString(L, "Values");
|
||||
Lua::NewTable(L);
|
||||
for (int i = 0; i < e.enumCount; ++i) {
|
||||
Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Lua::NewTable(L);
|
||||
Lua::SetFieldString(L, "Name", e.enumEntries[i].key);
|
||||
Lua::SetFieldString(L, "Type", "number");
|
||||
Lua::SetFieldNumber(L, "Value", e.enumEntries[i].value);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
// Pushes { Name, Type = "Structure" [, Documentation], Fields = { … } }.
|
||||
void PushStructure(void *L, const Structure &s) {
|
||||
Lua::NewTable(L);
|
||||
@@ -479,10 +507,13 @@ void PushSystem(void *L, const SystemInfo &sys) {
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
for (const Entry &e : g_entries) {
|
||||
if (e.kind != KIND_ENUM || !SameTextNoCase(e.enumSystem, sys.name))
|
||||
if (!IsValueTable(e.kind) || !SameTextNoCase(e.enumSystem, sys.name))
|
||||
continue;
|
||||
Lua::PushNumber(L, static_cast<double>(++n));
|
||||
PushEnumeration(L, e);
|
||||
if (e.kind == KIND_ENUM)
|
||||
PushEnumeration(L, e);
|
||||
else
|
||||
PushConstants(L, e);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
Lua::SetTable(L, -3);
|
||||
@@ -527,6 +558,7 @@ std::vector<std::string> Undocumented() {
|
||||
break;
|
||||
}
|
||||
case KIND_ENUM:
|
||||
case KIND_CONSTANTS:
|
||||
if (e.enumSystem == nullptr)
|
||||
out.push_back(std::string(e.table) + "." + e.name);
|
||||
break;
|
||||
@@ -633,10 +665,15 @@ void RecordTable(const char *table, const char *name, const Function *doc) {
|
||||
Record(KIND_TABLE, table, name, nullptr, doc);
|
||||
}
|
||||
|
||||
void RecordFrameMethods(const void *registry, const Game::Lua::FrameMethodEntry *table,
|
||||
int count, const Method *docs, int docCount) {
|
||||
void RecordFrameMethods(const void *registry,
|
||||
Game::ArrayRef<Game::Lua::FrameMethodEntry> methods,
|
||||
Game::Doc::MethodList docList) {
|
||||
if (g_pass == ENV_NONE)
|
||||
return;
|
||||
const Game::Lua::FrameMethodEntry *table = methods.data;
|
||||
const int count = methods.count;
|
||||
const Method *docs = docList.data;
|
||||
const int docCount = docList.count;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const Function *doc = nullptr;
|
||||
for (int d = 0; d < docCount; ++d) {
|
||||
@@ -662,18 +699,21 @@ void NameWidgetSystem(const void *registry, const char *system) {
|
||||
g_ownWidgetSystems.push_back({reinterpret_cast<uintptr_t>(registry), system});
|
||||
}
|
||||
|
||||
void RecordEnum(const char *parent, const char *sub, const Game::Lua::EnumIntegerEntry *entries,
|
||||
int count, const char *system) {
|
||||
namespace {
|
||||
|
||||
void RecordValueTable(Kind kind, const char *parent, const char *sub,
|
||||
const Game::Lua::EnumIntegerEntry *entries, int count,
|
||||
const char *system) {
|
||||
if (g_pass == ENV_NONE || sub == nullptr)
|
||||
return;
|
||||
if (Entry *e = Find(KIND_ENUM, parent, sub, nullptr)) {
|
||||
if (Entry *e = Find(kind, parent, sub, nullptr)) {
|
||||
e->env |= g_pass;
|
||||
if (e->enumSystem == nullptr)
|
||||
e->enumSystem = system;
|
||||
return;
|
||||
}
|
||||
Entry e{};
|
||||
e.kind = KIND_ENUM;
|
||||
e.kind = kind;
|
||||
e.env = g_pass;
|
||||
e.table = parent;
|
||||
e.name = sub;
|
||||
@@ -683,6 +723,18 @@ void RecordEnum(const char *parent, const char *sub, const Game::Lua::EnumIntege
|
||||
g_entries.push_back(e);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void RecordEnum(const char *parent, const char *sub,
|
||||
Game::ArrayRef<Game::Lua::EnumIntegerEntry> entries, const char *system) {
|
||||
RecordValueTable(KIND_ENUM, parent, sub, entries.data, entries.count, system);
|
||||
}
|
||||
|
||||
void RecordConstants(const char *sub, Game::ArrayRef<Game::Lua::EnumIntegerEntry> entries,
|
||||
const char *system) {
|
||||
RecordValueTable(KIND_CONSTANTS, "Constants", sub, entries.data, entries.count, system);
|
||||
}
|
||||
|
||||
} // namespace Api::Documentation
|
||||
|
||||
// The Structure constructor lives here so the list head is private to this
|
||||
|
||||
@@ -32,12 +32,15 @@ void EndPass();
|
||||
|
||||
void RecordGlobal(const char *name, const Game::Doc::Function *doc);
|
||||
void RecordTable(const char *table, const char *name, const Game::Doc::Function *doc);
|
||||
void RecordFrameMethods(const void *registry, const Game::Lua::FrameMethodEntry *table,
|
||||
int count, const Game::Doc::Method *docs, int docCount);
|
||||
void RecordFrameMethods(const void *registry,
|
||||
Game::ArrayRef<Game::Lua::FrameMethodEntry> methods,
|
||||
Game::Doc::MethodList docs);
|
||||
// Names the widget system of a method hash this DLL owns (a frame class it
|
||||
// adds), alongside the engine registries the exporter already knows.
|
||||
void NameWidgetSystem(const void *registry, const char *system);
|
||||
void RecordEnum(const char *parent, const char *sub, const Game::Lua::EnumIntegerEntry *entries,
|
||||
int count, const char *system);
|
||||
void RecordEnum(const char *parent, const char *sub,
|
||||
Game::ArrayRef<Game::Lua::EnumIntegerEntry> entries, const char *system);
|
||||
void RecordConstants(const char *sub, Game::ArrayRef<Game::Lua::EnumIntegerEntry> entries,
|
||||
const char *system);
|
||||
|
||||
} // namespace Api::Documentation
|
||||
|
||||
+14
-190
@@ -702,216 +702,40 @@ const Game::Doc::Function kCancelUnitBuff{
|
||||
"Cancels one of your own auras by index.", kCancelUnitBuffArgs, {},
|
||||
"SpellGlobals"};
|
||||
|
||||
const Game::Doc::Field kAuraDataFields[] = {
|
||||
Game::Doc::Req("name", "string"),
|
||||
Game::Doc::Req("icon", "string", "The icon's texture path."),
|
||||
Game::Doc::Req("applications", "number", "The stack count."),
|
||||
Game::Doc::Req("spellId", "number"),
|
||||
Game::Doc::Req("dispelName", "string",
|
||||
"Magic, Curse, Disease, or Poison; empty when the aura has no dispel type."),
|
||||
Game::Doc::Req("isHelpful", "bool"),
|
||||
Game::Doc::Req("isHarmful", "bool"),
|
||||
Game::Doc::Req("duration", "number", "In seconds; 0 when the duration is not known."),
|
||||
Game::Doc::Req("expirationTime", "number",
|
||||
"When the aura ends, in GetTime() seconds; 0 when not known."),
|
||||
Game::Doc::Opt("sourceUnit", "UnitToken", nullptr,
|
||||
"Nil when the cast was not seen or the caster has no unit token."),
|
||||
Game::Doc::Opt("sourceGUID", "WOWGUID", nullptr,
|
||||
"The caster's GUID, a ClassicAPI extension; nil when the cast was not seen."),
|
||||
Game::Doc::Opt("auraInstanceID", "number", nullptr,
|
||||
"Nil for an aura that is not in the unit's aura list."),
|
||||
Game::Doc::Req("charges", "number", "Always 0."),
|
||||
Game::Doc::Req("maxCharges", "number", "Always 0."),
|
||||
Game::Doc::Req("timeMod", "number", "Always 1."),
|
||||
Game::Doc::Req("isFromPlayerOrPlayerPet", "bool",
|
||||
"True when any player or a player's pet applied the aura."),
|
||||
Game::Doc::Req("canApplyAura", "bool",
|
||||
"True when the player or the pet knows the spell that applies the aura."),
|
||||
Game::Doc::Req("isRaid", "bool", "True when the aura passes the RAID filter."),
|
||||
Game::Doc::Req("isStealable", "bool",
|
||||
"True when the player can remove this buff from an enemy."),
|
||||
Game::Doc::Req("isBossAura", "bool", "Always false."),
|
||||
Game::Doc::Req("isNameplateOnly", "bool", "Always false."),
|
||||
Game::Doc::Req("nameplateShowAll", "bool", "Always false."),
|
||||
Game::Doc::Req("nameplateShowPersonal", "bool", "Always false."),
|
||||
Game::Doc::Req("shouldConsolidate", "bool", "Always false."),
|
||||
};
|
||||
const Game::Doc::Structure kAuraData{"AuraData", "UnitAuras", kAuraDataFields,
|
||||
"One aura on a unit."};
|
||||
|
||||
// The Classic Era `UnitAura` tuple, shared by the four positional getters.
|
||||
// `Data::PushPositional` is the source of the order.
|
||||
const Game::Doc::Field kPositionalRets[] = {
|
||||
Game::Doc::Opt("name", "string", nullptr,
|
||||
"Nil, as the only return, when there is no such aura."),
|
||||
Game::Doc::Opt("icon", "string"),
|
||||
Game::Doc::Opt("count", "number"),
|
||||
Game::Doc::Opt("dispelType", "string", nullptr,
|
||||
"Empty when the aura has no dispel type."),
|
||||
Game::Doc::Opt("duration", "number"),
|
||||
Game::Doc::Opt("expirationTime", "number"),
|
||||
Game::Doc::Opt("source", "UnitToken", nullptr, "Nil when the caster is not known."),
|
||||
Game::Doc::Opt("isStealable", "bool"),
|
||||
Game::Doc::Opt("nameplateShowPersonal", "bool", nullptr, "Always false."),
|
||||
Game::Doc::Opt("spellId", "number"),
|
||||
Game::Doc::Opt("canApplyAura", "bool"),
|
||||
Game::Doc::Opt("isBossDebuff", "bool", nullptr, "Always false."),
|
||||
Game::Doc::Opt("castByPlayer", "bool", nullptr,
|
||||
"True when a player applied the aura; false for a pet."),
|
||||
Game::Doc::Opt("nameplateShowAll", "bool", nullptr, "Always false."),
|
||||
Game::Doc::Opt("timeMod", "number", nullptr, "Always 1."),
|
||||
};
|
||||
|
||||
const Game::Doc::Field kAuraRets[] = {
|
||||
Game::Doc::Opt("aura", "AuraData", nullptr, "Nil when there is no such aura."),
|
||||
};
|
||||
|
||||
const Game::Doc::Field kByIndexArgs[] = {
|
||||
Game::Doc::Req("unit", "UnitToken"),
|
||||
Game::Doc::Req("index", "luaIndex"),
|
||||
Game::Doc::Opt("filter", "AuraFilters", nullptr,
|
||||
"Tokens such as HELPFUL, HARMFUL, PLAYER, RAID; HELPFUL when no range is given."),
|
||||
};
|
||||
const Game::Doc::Function kGetAuraDataByIndex{
|
||||
"Returns the aura at an index among a unit's auras that match a filter.", kByIndexArgs,
|
||||
kAuraRets};
|
||||
const Game::Doc::Function kUnitAura{
|
||||
"Returns the aura at an index as positional values, without making a table.",
|
||||
kByIndexArgs, kPositionalRets};
|
||||
|
||||
const Game::Doc::Field kLockedIndexArgs[] = {
|
||||
Game::Doc::Req("unit", "UnitToken"),
|
||||
Game::Doc::Req("index", "luaIndex"),
|
||||
};
|
||||
const Game::Doc::Function kGetBuffDataByIndex{
|
||||
"Returns the buff at an index on a unit.", kLockedIndexArgs, kAuraRets};
|
||||
const Game::Doc::Function kGetDebuffDataByIndex{
|
||||
"Returns the debuff at an index on a unit.", kLockedIndexArgs, kAuraRets};
|
||||
|
||||
const Game::Doc::Field kLockedFilterArgs[] = {
|
||||
Game::Doc::Req("unit", "UnitToken"),
|
||||
Game::Doc::Req("index", "luaIndex"),
|
||||
Game::Doc::Opt("filter", "AuraFilters", nullptr,
|
||||
"Further tokens such as PLAYER or RAID; the range is fixed."),
|
||||
};
|
||||
const Game::Doc::Function kUnitBuff{
|
||||
"Returns the buff at an index as positional values, without making a table.",
|
||||
kLockedFilterArgs, kPositionalRets};
|
||||
const Game::Doc::Function kUnitDebuff{
|
||||
"Returns the debuff at an index as positional values, without making a table.",
|
||||
kLockedFilterArgs, kPositionalRets};
|
||||
|
||||
const Game::Doc::Field kSlotsArgs[] = {
|
||||
Game::Doc::Req("unit", "UnitToken"),
|
||||
Game::Doc::Opt("filter", "AuraFilters"),
|
||||
Game::Doc::Opt("maxSlots", "number", nullptr, "Nil or 0 returns all of them."),
|
||||
Game::Doc::Opt("continuationToken", "number", nullptr,
|
||||
"The first return of the previous call, to get the next batch."),
|
||||
Game::Doc::Opt("buf", "table", nullptr,
|
||||
"A table to fill with the slot IDs instead; the call then returns the "
|
||||
"token and the count. A ClassicAPI extension."),
|
||||
};
|
||||
const Game::Doc::Field kSlotsRets[] = {
|
||||
Game::Doc::Opt("outContinuationToken", "number", nullptr,
|
||||
"Nil when this batch reaches the last aura."),
|
||||
Game::Doc::Vararg("number", "The slot IDs, valid only in the frame they are returned."),
|
||||
};
|
||||
const Game::Doc::Function kGetAuraSlots{
|
||||
"Returns the slot IDs of a unit's auras that match a filter, in batches.", kSlotsArgs,
|
||||
kSlotsRets};
|
||||
|
||||
const Game::Doc::Field kBySlotArgs[] = {
|
||||
Game::Doc::Req("unit", "UnitToken"),
|
||||
Game::Doc::Req("slot", "number", "A slot ID from C_UnitAuras.GetAuraSlots."),
|
||||
};
|
||||
const Game::Doc::Function kGetAuraDataBySlot{
|
||||
"Returns the aura that a slot ID names.", kBySlotArgs, kAuraRets};
|
||||
const Game::Doc::Function kUnitAuraBySlot{
|
||||
"Returns the aura that a slot ID names as positional values, without making a table.",
|
||||
kBySlotArgs, kPositionalRets};
|
||||
|
||||
const Game::Doc::Field kBySpellIDArgs[] = {
|
||||
Game::Doc::Req("unit", "UnitToken"),
|
||||
Game::Doc::Req("spellID", "number"),
|
||||
Game::Doc::Opt("filter", "AuraFilters", nullptr, "Nil searches buffs, then debuffs."),
|
||||
};
|
||||
const Game::Doc::Function kGetUnitAuraBySpellID{
|
||||
"Returns the first aura on a unit with a spell ID; the player's own aura comes first.",
|
||||
kBySpellIDArgs, kAuraRets};
|
||||
|
||||
const Game::Doc::Field kPlayerBySpellIDArgs[] = {
|
||||
Game::Doc::Req("spellID", "number"),
|
||||
};
|
||||
const Game::Doc::Function kGetPlayerAuraBySpellID{
|
||||
"Returns the first aura on the player with a spell ID.", kPlayerBySpellIDArgs, kAuraRets};
|
||||
|
||||
const Game::Doc::Field kBySpellNameArgs[] = {
|
||||
Game::Doc::Req("unit", "UnitToken"),
|
||||
Game::Doc::Req("spellName", "cstring", "The localized name; case does not matter."),
|
||||
Game::Doc::Opt("filter", "AuraFilters", nullptr, "Nil searches buffs, then debuffs."),
|
||||
};
|
||||
const Game::Doc::Function kGetAuraDataBySpellName{
|
||||
"Returns the first aura on a unit with a spell name; the player's own aura comes first.",
|
||||
kBySpellNameArgs, kAuraRets};
|
||||
|
||||
const Game::Doc::Field kUnitAurasArgs[] = {
|
||||
Game::Doc::Req("unit", "UnitToken"),
|
||||
Game::Doc::Opt("filter", "AuraFilters", nullptr,
|
||||
"With no HELPFUL or HARMFUL token, both are returned."),
|
||||
};
|
||||
const Game::Doc::Field kUnitAurasRets[] = {
|
||||
Game::Doc::Req("auras", "table", "An array of AuraData; empty when the unit has none."),
|
||||
};
|
||||
const Game::Doc::Function kGetUnitAuras{
|
||||
"Returns all of a unit's auras that match a filter, buffs first.", kUnitAurasArgs,
|
||||
kUnitAurasRets};
|
||||
|
||||
const Game::Doc::Field kDispelColorArgs[] = {
|
||||
Game::Doc::Opt("dispelName", "string", nullptr,
|
||||
"Magic, Curse, Disease, Poison, or Enrage; any other value gives the "
|
||||
"no-type color."),
|
||||
};
|
||||
const Game::Doc::Field kDispelColorRets[] = {
|
||||
Game::Doc::Req("color", "colorRGBA"),
|
||||
};
|
||||
const Game::Doc::Function kGetAuraDispelTypeColor{
|
||||
"Returns the border color for an aura's dispel type.", kDispelColorArgs,
|
||||
kDispelColorRets};
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataByIndex",
|
||||
&Script_GetAuraDataByIndex, &kGetAuraDataByIndex);
|
||||
&Script_GetAuraDataByIndex);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetBuffDataByIndex",
|
||||
&Script_GetBuffDataByIndex, &kGetBuffDataByIndex);
|
||||
&Script_GetBuffDataByIndex);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetDebuffDataByIndex",
|
||||
&Script_GetDebuffDataByIndex, &kGetDebuffDataByIndex);
|
||||
&Script_GetDebuffDataByIndex);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitAura",
|
||||
&Script_UnitAura, &kUnitAura);
|
||||
&Script_UnitAura);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitBuff",
|
||||
&Script_UnitBuff, &kUnitBuff);
|
||||
&Script_UnitBuff);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitDebuff",
|
||||
&Script_UnitDebuff, &kUnitDebuff);
|
||||
&Script_UnitDebuff);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraSlots",
|
||||
&Script_GetAuraSlots, &kGetAuraSlots);
|
||||
&Script_GetAuraSlots);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataBySlot",
|
||||
&Script_GetAuraDataBySlot, &kGetAuraDataBySlot);
|
||||
&Script_GetAuraDataBySlot);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataByAuraInstanceID",
|
||||
&Script_GetAuraDataByAuraInstanceID,
|
||||
&kGetAuraDataByAuraInstanceID);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitAuraBySlot",
|
||||
&Script_UnitAuraBySlot, &kUnitAuraBySlot);
|
||||
&Script_UnitAuraBySlot);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetUnitAuraBySpellID",
|
||||
&Script_GetUnitAuraBySpellID, &kGetUnitAuraBySpellID);
|
||||
&Script_GetUnitAuraBySpellID);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetPlayerAuraBySpellID",
|
||||
&Script_GetPlayerAuraBySpellID, &kGetPlayerAuraBySpellID);
|
||||
&Script_GetPlayerAuraBySpellID);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataBySpellName",
|
||||
&Script_GetAuraDataBySpellName, &kGetAuraDataBySpellName);
|
||||
&Script_GetAuraDataBySpellName);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetUnitAuras",
|
||||
&Script_GetUnitAuras, &kGetUnitAuras);
|
||||
&Script_GetUnitAuras);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDispelTypeColor",
|
||||
&Script_GetAuraDispelTypeColor, &kGetAuraDispelTypeColor);
|
||||
&Script_GetAuraDispelTypeColor);
|
||||
Game::Lua::RegisterGlobalFunction("CancelUnitBuff", &Script_CancelUnitBuff,
|
||||
&kCancelUnitBuff);
|
||||
}
|
||||
|
||||
@@ -193,23 +193,9 @@ int __fastcall Script_RegisterComboDuration(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("spellID", "number"),
|
||||
Game::Doc::Req("baseSeconds", "number",
|
||||
"The duration at zero combo points; each point adds a fifth of the "
|
||||
"difference to maxSeconds."),
|
||||
Game::Doc::Req("maxSeconds", "number", "The duration at five combo points."),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Req("success", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kRegisterComboDuration{
|
||||
"Sets the combo point durations of a finisher whose client data is missing or wrong.",
|
||||
kArgs, kRets, nullptr, true};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "RegisterComboDuration",
|
||||
&Script_RegisterComboDuration, &kRegisterComboDuration);
|
||||
&Script_RegisterComboDuration);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
+3
-31
@@ -946,31 +946,10 @@ int __fastcall Script_RegisterAuraDurationModifierByTrigger(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kDurationModArgs[] = {
|
||||
Game::Doc::Req("triggerFamily", "number", "The SpellFamilyName of the triggering cast."),
|
||||
Game::Doc::Req("triggerSchool", "number",
|
||||
"The school index the trigger must have, or below 0 for any school."),
|
||||
Game::Doc::Req("affectedFamily", "number", "The SpellFamilyName of the aura to change."),
|
||||
Game::Doc::Req("affectedFamilyFlags", "number",
|
||||
"A SpellFamilyFlags mask the aura must overlap, or 0 to match on the icon."),
|
||||
Game::Doc::Req("affectedIcon", "number",
|
||||
"The SpellIconID the aura must have, or 0 for any icon."),
|
||||
Game::Doc::Req("op", "string", "refresh, reduce, set, or remove."),
|
||||
Game::Doc::Opt("valueSeconds", "number", nullptr, "The amount for reduce and set."),
|
||||
};
|
||||
const Game::Doc::Field kDurationModRets[] = {
|
||||
Game::Doc::Req("success", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kRegisterAuraDurationModifierByTrigger{
|
||||
"Changes the shown duration of the caster's aura on a unit when a matching cast by "
|
||||
"that caster lands.",
|
||||
kDurationModArgs, kDurationModRets, nullptr, true};
|
||||
|
||||
void RegisterDurationModLua() {
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras",
|
||||
"RegisterAuraDurationModifierByTrigger",
|
||||
&Script_RegisterAuraDurationModifierByTrigger,
|
||||
&kRegisterAuraDurationModifierByTrigger);
|
||||
&Script_RegisterAuraDurationModifierByTrigger);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoregDurationMod{&RegisterDurationModLua};
|
||||
@@ -1044,16 +1023,9 @@ CompressState *FindOrClaimCompressState(uint64_t targetGuid,
|
||||
// would compress it, this gate excludes it exactly as the server's does.
|
||||
// (Drain Soul passes via its effect-2 periodic damage: aura=[86,3,0].)
|
||||
bool HasPeriodicDamageOrLeech(const uint8_t *rec) {
|
||||
if (rec == nullptr)
|
||||
return false;
|
||||
constexpr int32_t kPeriodicDamage = 3, kPeriodicLeech = 53;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
const int32_t aura = *reinterpret_cast<const int32_t *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME + i * 4);
|
||||
if (aura == kPeriodicDamage || aura == kPeriodicLeech)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return Spell::Lookup::HasAuraEffect(rec, kPeriodicDamage) ||
|
||||
Spell::Lookup::HasAuraEffect(rec, kPeriodicLeech);
|
||||
}
|
||||
|
||||
// Compression pairs by CASTER, mirroring the server (each warlock's Dark
|
||||
|
||||
@@ -0,0 +1,468 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// C_AutoComplete: the player names an edit box can complete.
|
||||
//
|
||||
// 4.3.4's CGAutoCompleteName keeps its own list of names, each with flag bits
|
||||
// that the party, raid, guild, friend list and chat code add and remove as
|
||||
// those lists change. 1.12 has no such list, but it keeps every list the bits
|
||||
// come from, so a query reads them where the engine keeps them:
|
||||
// - party: the four VAR_PARTY_GUIDS, the name from the name cache, online
|
||||
// from the member stats block (Group::MemberStats) - InGroup;
|
||||
// - raid: the VAR_RAID_MEMBER_PTRS slots and their online bit - InGroup;
|
||||
// - guild roster: each GuildMemberInfo's GUID, name and online word, read
|
||||
// only while the player is in a guild, the test Script_IsInGuild
|
||||
// (0x00516DE0) makes, because the engine keeps the old roster after a
|
||||
// /gquit - InGuild;
|
||||
// - friends list: the social singleton's slots (FriendList.h) - Friend.
|
||||
// The one list the engine does not keep is ours: the players this character
|
||||
// whispered or was whispered by (Chat::Dispatch reports each line), in the
|
||||
// order they were last heard from - InteractedWith. Logging out clears it,
|
||||
// as 4.3.4's ShutdownGame clears its list.
|
||||
//
|
||||
// A player on several lists is one entry with every list's bit. A list that
|
||||
// holds the player decides Online; a player known only from a whisper is
|
||||
// Online, as 4.3.4 sets Online together with InteractedWith.
|
||||
//
|
||||
// Matching is 4.3.4's NodeMatchesParams (the same in 3.3.5's binary,
|
||||
// 0x0057ACA0), with two differences:
|
||||
// - a cursorPosition outside 1..length means the end of the text. Retail's
|
||||
// tell-target extraction (ChatFrameEditBox.lua) passes 0 and expects a
|
||||
// whole-text match;
|
||||
// - the edit-from-center search also tries the last place. The engine's
|
||||
// loop stops one place early, so it never finds the text after the
|
||||
// cursor at the end of a name.
|
||||
// Order is 3.3.5's SortNameNode (0x0057B130): with
|
||||
// autoCompleteResortNamesOnRecency on, the most recent whisper first, then by
|
||||
// name. Retail's client cannot be read, so each result's priority is the
|
||||
// highest Enum.AutoCompletePriority its bits reach, in the enum's own order:
|
||||
// Friend, Guild, InGroup, Interacted, else Other.
|
||||
|
||||
#include "autocomplete/Names.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "cvar/Factory.h"
|
||||
#include "friendlist/FriendList.h"
|
||||
#include "group/MemberStats.h"
|
||||
#include "guid/Guid.h"
|
||||
#include "text/Utf8.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace AutoComplete::Names {
|
||||
|
||||
namespace {
|
||||
|
||||
// Enum.AutoCompleteEntryFlag. 1.12 has nothing to fill Bnet, InAOI,
|
||||
// AccountCharacter or RecentPlayer from.
|
||||
enum EntryFlag : uint32_t {
|
||||
FLAG_IN_GROUP = 0x1,
|
||||
FLAG_IN_GUILD = 0x2,
|
||||
FLAG_FRIEND = 0x4,
|
||||
FLAG_BNET = 0x8,
|
||||
FLAG_INTERACTED_WITH = 0x10,
|
||||
FLAG_ONLINE = 0x20,
|
||||
FLAG_IN_AOI = 0x40,
|
||||
FLAG_ACCOUNT_CHARACTER = 0x80,
|
||||
FLAG_RECENT_PLAYER = 0x100,
|
||||
};
|
||||
|
||||
// Enum.AutoCompletePriority.
|
||||
enum Priority {
|
||||
PRIORITY_OTHER = 0,
|
||||
PRIORITY_INTERACTED = 1,
|
||||
PRIORITY_IN_GROUP = 2,
|
||||
PRIORITY_GUILD = 3,
|
||||
PRIORITY_FRIEND = 4,
|
||||
PRIORITY_ACCOUNT_CHARACTER = 5,
|
||||
PRIORITY_ACCOUNT_CHARACTER_SAME_REALM = 6,
|
||||
};
|
||||
|
||||
constexpr int CHAT_MSG_WHISPER = 6;
|
||||
constexpr int CHAT_MSG_WHISPER_INFORM = 7;
|
||||
constexpr int LANGUAGE_ADDON = -1; // the dispatch's addon-message arm
|
||||
|
||||
CVar::Factory::Handle g_resortOnRecency = nullptr;
|
||||
CVar::Factory::Handle g_editFromCenter = nullptr;
|
||||
CVar::Factory::Handle g_useContext = nullptr;
|
||||
|
||||
bool CVarOn(CVar::Factory::Handle cvar) { return CVar::Factory::GetInt(cvar, 1) != 0; }
|
||||
|
||||
// A whisper partner. `order` grows with every whisper, so a larger one was
|
||||
// heard from more recently.
|
||||
struct Interaction {
|
||||
uint64_t guid;
|
||||
std::string name;
|
||||
uint32_t order;
|
||||
};
|
||||
|
||||
std::vector<Interaction> g_interactions;
|
||||
uint32_t g_lastOrder = 0;
|
||||
|
||||
// One player as one list knows them, or, after Merge, as all lists do.
|
||||
struct Entry {
|
||||
uint64_t guid;
|
||||
const char *name; // the list's own storage, valid until Lua runs
|
||||
uint32_t flags; // EntryFlag bits, Online excluded
|
||||
bool listed; // a group, guild or friends list holds the player
|
||||
bool online; // what those lists say
|
||||
uint32_t order; // the Interaction's order, 0 for none
|
||||
};
|
||||
|
||||
bool InGuild() {
|
||||
const uint8_t *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr)
|
||||
return false;
|
||||
const auto *info = Game::Read<const uint8_t *>(player, Offsets::OFF_CGPLAYER_INFO);
|
||||
return info != nullptr && Game::Read<uint32_t>(info, Offsets::OFF_PLAYER_INFO_GUILD_KEY) != 0;
|
||||
}
|
||||
|
||||
// Every player whose name `match` accepts, one entry per list that has them.
|
||||
template <typename Match>
|
||||
std::vector<Entry> Collect(Match match) {
|
||||
std::vector<Entry> found;
|
||||
const uint64_t self = Unit::Identity::PlayerGuid();
|
||||
const auto add = [&](uint64_t guid, const char *name, uint32_t flag, bool listed, bool online,
|
||||
uint32_t order) {
|
||||
if (guid == 0 || guid == self || name == nullptr || *name == '\0' || !match(name))
|
||||
return;
|
||||
found.push_back({guid, name, flag, listed, online, order});
|
||||
};
|
||||
|
||||
const auto *party =
|
||||
reinterpret_cast<const uint64_t *>(static_cast<uintptr_t>(Offsets::VAR_PARTY_GUIDS));
|
||||
for (int i = 0; i < Offsets::PARTY_MAX_SLOTS; ++i) {
|
||||
const uint64_t guid = party[i];
|
||||
if (guid != 0)
|
||||
add(guid, Unit::Identity::NameForGuid(guid), FLAG_IN_GROUP, true,
|
||||
Group::MemberStats::Lookup(guid).online, 0);
|
||||
}
|
||||
|
||||
int raidCount = Game::Read<int>(Offsets::VAR_RAID_MEMBER_COUNT);
|
||||
if (raidCount > Offsets::RAID_MAX_SLOTS)
|
||||
raidCount = Offsets::RAID_MAX_SLOTS;
|
||||
const auto *slots = reinterpret_cast<const uint8_t *const *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_RAID_MEMBER_PTRS));
|
||||
for (int i = 0; i < raidCount; ++i) {
|
||||
if (slots[i] == nullptr)
|
||||
continue;
|
||||
const uint64_t guid = Game::Read<uint64_t>(slots[i], 0);
|
||||
const uint32_t flags = Game::Read<uint32_t>(slots[i], Offsets::OFF_RAID_SLOT_FLAGS);
|
||||
add(guid, Unit::Identity::NameForGuid(guid), FLAG_IN_GROUP, true,
|
||||
(flags & Offsets::RAID_SLOT_FLAG_ONLINE) != 0, 0);
|
||||
}
|
||||
|
||||
if (InGuild()) {
|
||||
auto **roster = Game::Read<const uint8_t **>(Offsets::VAR_GUILD_ROSTER_PTR);
|
||||
const int total = Game::Read<int>(Offsets::VAR_GUILD_ROSTER_TOTAL_COUNT);
|
||||
for (int i = 0; roster != nullptr && i < total; ++i) {
|
||||
const uint8_t *member = roster[i];
|
||||
if (member == nullptr)
|
||||
continue;
|
||||
add(Game::Read<uint64_t>(member, Offsets::OFF_GUILD_MEMBER_GUID),
|
||||
reinterpret_cast<const char *>(member + Offsets::OFF_GUILD_MEMBER_NAME),
|
||||
FLAG_IN_GUILD, true,
|
||||
Game::Read<uint32_t>(member, Offsets::OFF_GUILD_MEMBER_ONLINE) != 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
const int friends = FriendList::Count();
|
||||
for (int i = 0; i < friends; ++i) {
|
||||
const uint8_t *entry = FriendList::EntryByIndex(i);
|
||||
if (entry == nullptr)
|
||||
break;
|
||||
add(Game::Read<uint64_t>(entry, FriendList::OFF_FRIEND_GUID),
|
||||
Game::Read<const char *>(entry, FriendList::OFF_FRIEND_NAME), FLAG_FRIEND, true,
|
||||
Game::Read<uint8_t>(entry, FriendList::OFF_FRIEND_CONNECTED) != 0, 0);
|
||||
}
|
||||
|
||||
for (const Interaction &i : g_interactions)
|
||||
add(i.guid, i.name.c_str(), FLAG_INTERACTED_WITH, false, false, i.order);
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
// One entry per player, with every list's bits and Online decided; then the
|
||||
// include / exclude test, which autoCompleteUseContext turns off.
|
||||
std::vector<Entry> Merge(std::vector<Entry> found, uint32_t include, uint32_t exclude) {
|
||||
std::sort(found.begin(), found.end(),
|
||||
[](const Entry &a, const Entry &b) { return a.guid < b.guid; });
|
||||
std::vector<Entry> merged;
|
||||
for (const Entry &e : found) {
|
||||
if (!merged.empty() && merged.back().guid == e.guid) {
|
||||
Entry &m = merged.back();
|
||||
m.flags |= e.flags;
|
||||
m.listed = m.listed || e.listed;
|
||||
m.online = m.online || e.online;
|
||||
m.order = std::max(m.order, e.order);
|
||||
} else {
|
||||
merged.push_back(e);
|
||||
}
|
||||
}
|
||||
|
||||
const bool useContext = CVarOn(g_useContext);
|
||||
std::vector<Entry> kept;
|
||||
for (Entry &e : merged) {
|
||||
if (e.listed ? e.online : (e.flags & FLAG_INTERACTED_WITH) != 0)
|
||||
e.flags |= FLAG_ONLINE;
|
||||
if (!useContext || ((e.flags & include) != 0 && (e.flags & exclude) == 0))
|
||||
kept.push_back(e);
|
||||
}
|
||||
return kept;
|
||||
}
|
||||
|
||||
int PriorityOf(uint32_t flags) {
|
||||
if (flags & FLAG_FRIEND)
|
||||
return PRIORITY_FRIEND;
|
||||
if (flags & FLAG_IN_GUILD)
|
||||
return PRIORITY_GUILD;
|
||||
if (flags & FLAG_IN_GROUP)
|
||||
return PRIORITY_IN_GROUP;
|
||||
if (flags & FLAG_INTERACTED_WITH)
|
||||
return PRIORITY_INTERACTED;
|
||||
return PRIORITY_OTHER;
|
||||
}
|
||||
|
||||
// Past `chars` code points of `s`, continuation bytes included, stopping at
|
||||
// the terminator. (Storm's SStrAdvanceCharsUTF8 stops one byte into a
|
||||
// multi-byte character.)
|
||||
const char *Advance(const char *s, size_t chars) {
|
||||
for (; *s != '\0' && chars != 0; --chars) {
|
||||
++s;
|
||||
while ((static_cast<uint8_t>(*s) & 0xC0) == 0x80)
|
||||
++s;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// What the edit box holds: the text and the cursor in it, in code points.
|
||||
struct Typed {
|
||||
const char *text;
|
||||
size_t length;
|
||||
size_t cursor;
|
||||
bool allowFullMatch;
|
||||
bool editFromCenter;
|
||||
};
|
||||
|
||||
bool Completes(const Typed &t, const char *name) {
|
||||
if (!t.allowFullMatch && Text::Utf8::CompareI(t.text, name) == 0)
|
||||
return false;
|
||||
if (!t.editFromCenter || t.cursor == t.length)
|
||||
return Text::Utf8::CompareI(t.text, name, t.length) == 0;
|
||||
|
||||
// The text before the cursor starts the name, and the text after it
|
||||
// follows somewhere later in the name.
|
||||
if (Text::Utf8::CompareI(t.text, name, t.cursor) != 0)
|
||||
return false;
|
||||
const size_t nameLength = Text::Utf8::Length(name);
|
||||
if (nameLength < t.length)
|
||||
return false;
|
||||
const char *tail = Advance(t.text, t.cursor);
|
||||
const size_t tailLength = t.length - t.cursor;
|
||||
const char *at = Advance(name, t.cursor);
|
||||
for (size_t places = nameLength - t.length + 1; places != 0; --places) {
|
||||
if (Text::Utf8::CompareI(at, tail, tailLength) == 0)
|
||||
return true;
|
||||
at = Advance(at, 1);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// A flag mask from Lua: bit.bor results above 0x7FFFFFFF arrive negative.
|
||||
uint32_t FlagArg(void *L, int index) {
|
||||
return static_cast<uint32_t>(static_cast<int64_t>(Game::Lua::ToNumber(L, index)));
|
||||
}
|
||||
|
||||
// C_AutoComplete.GetAutoCompleteResults(name, numResults, cursorPosition,
|
||||
// allowFullMatch, includeFlags, excludeFlags) -> results
|
||||
// A numResults of 0 or less returns every match, as the engine's count test
|
||||
// (`++count == numReturns`) does.
|
||||
int __fastcall Script_GetAutoCompleteResults(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1) || !Game::Lua::IsNumber(L, 2) || !Game::Lua::IsNumber(L, 3) ||
|
||||
!Game::Lua::IsNumber(L, 5) || !Game::Lua::IsNumber(L, 6)) {
|
||||
Game::Lua::Error(L, "Usage: C_AutoComplete.GetAutoCompleteResults(name, numResults, "
|
||||
"cursorPosition, allowFullMatch, includeFlags, excludeFlags)");
|
||||
return 0;
|
||||
}
|
||||
Typed typed;
|
||||
typed.text = Game::Lua::ToString(L, 1);
|
||||
typed.length = Text::Utf8::Length(typed.text);
|
||||
const double cursor = Game::Lua::ToNumber(L, 3);
|
||||
typed.cursor = cursor >= 1 && cursor <= static_cast<double>(typed.length)
|
||||
? static_cast<size_t>(cursor)
|
||||
: typed.length;
|
||||
typed.allowFullMatch = Game::Lua::ToBoolean(L, 4) != 0;
|
||||
typed.editFromCenter = CVarOn(g_editFromCenter);
|
||||
const int numResults = static_cast<int>(Game::Lua::ToNumber(L, 2));
|
||||
|
||||
std::vector<Entry> matches =
|
||||
Merge(Collect([&](const char *name) { return Completes(typed, name); }), FlagArg(L, 5),
|
||||
FlagArg(L, 6));
|
||||
const bool recency = CVarOn(g_resortOnRecency);
|
||||
std::sort(matches.begin(), matches.end(), [recency](const Entry &a, const Entry &b) {
|
||||
if (recency && a.order != b.order)
|
||||
return a.order > b.order;
|
||||
return Text::Utf8::CompareI(a.name, b.name) < 0;
|
||||
});
|
||||
if (numResults > 0 && matches.size() > static_cast<size_t>(numResults))
|
||||
matches.resize(numResults);
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
for (size_t i = 0; i < matches.size(); ++i) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldString(L, "name", matches[i].name);
|
||||
Game::Lua::SetFieldNumber(L, "priority", PriorityOf(matches[i].flags));
|
||||
Game::Lua::SetFieldNumber(L, "bnetID", 0);
|
||||
Game::Lua::RawSet(L, -3);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// C_AutoComplete.IsRecognizedName(name, includeFlags, excludeFlags) -> bool
|
||||
int __fastcall Script_IsRecognizedName(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1) || !Game::Lua::IsNumber(L, 2) || !Game::Lua::IsNumber(L, 3)) {
|
||||
Game::Lua::Error(L,
|
||||
"Usage: C_AutoComplete.IsRecognizedName(name, includeFlags, excludeFlags)");
|
||||
return 0;
|
||||
}
|
||||
const char *text = Game::Lua::ToString(L, 1);
|
||||
const std::vector<Entry> matches =
|
||||
Merge(Collect([text](const char *name) { return Text::Utf8::CompareI(text, name) == 0; }),
|
||||
FlagArg(L, 2), FlagArg(L, 3));
|
||||
Game::Lua::PushBool(L, !matches.empty());
|
||||
return 1;
|
||||
}
|
||||
|
||||
void ClearOnLogout() {
|
||||
g_interactions.clear();
|
||||
g_lastOrder = 0;
|
||||
}
|
||||
|
||||
const Game::GlueModuleAutoRegister _clearOnLogout{&ClearOnLogout};
|
||||
|
||||
// --- Documentation ----------------------------------------------------------
|
||||
|
||||
const Game::Doc::Field kResultFields[] = {
|
||||
Game::Doc::Req("name", "string"),
|
||||
Game::Doc::Req("priority", "AutoCompletePriority",
|
||||
"The closest tie to the player: friend, guild, group, whisper, or none."),
|
||||
Game::Doc::Req("bnetID", "number", "Always 0."),
|
||||
};
|
||||
const Game::Doc::Structure kResult{"AutoCompleteResult", "AutoComplete", kResultFields};
|
||||
|
||||
const Game::Doc::Field kResultsArgs[] = {
|
||||
Game::Doc::Req("name", "string", "The text typed so far."),
|
||||
Game::Doc::Req("numResults", "number", "The most results to return; 0 returns all."),
|
||||
Game::Doc::Req("cursorPosition", "number", "The cursor's place in the text, in characters."),
|
||||
Game::Doc::Req("allowFullMatch", "bool", "Whether a name equal to the text can be a result."),
|
||||
Game::Doc::Req("includeFlags", "number",
|
||||
"Enum.AutoCompleteEntryFlag bits; a result has at least one."),
|
||||
Game::Doc::Req("excludeFlags", "number",
|
||||
"Enum.AutoCompleteEntryFlag bits; a result has none."),
|
||||
};
|
||||
const Game::Doc::Field kResultsRets[] = {
|
||||
Game::Doc::Req("results", "table", "Array of AutoCompleteResult."),
|
||||
};
|
||||
const Game::Doc::Function kGetAutoCompleteResults{
|
||||
"The known players whose names complete the typed text.", kResultsArgs, kResultsRets};
|
||||
|
||||
const Game::Doc::Field kRecognizedArgs[] = {
|
||||
Game::Doc::Req("name", "string"),
|
||||
Game::Doc::Req("includeFlags", "number",
|
||||
"Enum.AutoCompleteEntryFlag bits; the player has at least one."),
|
||||
Game::Doc::Req("excludeFlags", "number",
|
||||
"Enum.AutoCompleteEntryFlag bits; the player has none."),
|
||||
};
|
||||
const Game::Doc::Field kRecognizedRets[] = {
|
||||
Game::Doc::Req("isRecognizedName", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kIsRecognizedName{"Whether a known player has exactly this name.",
|
||||
kRecognizedArgs, kRecognizedRets};
|
||||
|
||||
const Game::Lua::EnumIntegerEntry kEntryFlagEntries[] = {
|
||||
{"InGroup", FLAG_IN_GROUP},
|
||||
{"InGuild", FLAG_IN_GUILD},
|
||||
{"Friend", FLAG_FRIEND},
|
||||
{"Bnet", FLAG_BNET},
|
||||
{"InteractedWith", FLAG_INTERACTED_WITH},
|
||||
{"Online", FLAG_ONLINE},
|
||||
{"InAOI", FLAG_IN_AOI},
|
||||
{"AccountCharacter", FLAG_ACCOUNT_CHARACTER},
|
||||
{"RecentPlayer", FLAG_RECENT_PLAYER},
|
||||
};
|
||||
|
||||
const Game::Lua::EnumIntegerEntry kPriorityEntries[] = {
|
||||
{"Other", PRIORITY_OTHER},
|
||||
{"Interacted", PRIORITY_INTERACTED},
|
||||
{"InGroup", PRIORITY_IN_GROUP},
|
||||
{"Guild", PRIORITY_GUILD},
|
||||
{"Friend", PRIORITY_FRIEND},
|
||||
{"AccountCharacter", PRIORITY_ACCOUNT_CHARACTER},
|
||||
{"AccountCharacterSameRealm", PRIORITY_ACCOUNT_CHARACTER_SAME_REALM},
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void OnChat(int chatType, int languageID, const char *name, uint64_t guid) {
|
||||
if (chatType != CHAT_MSG_WHISPER && chatType != CHAT_MSG_WHISPER_INFORM)
|
||||
return;
|
||||
if (languageID == LANGUAGE_ADDON || guid == 0 || !Guid::IsPlayer(guid) || name == nullptr ||
|
||||
*name == '\0')
|
||||
return;
|
||||
++g_lastOrder;
|
||||
for (Interaction &i : g_interactions) {
|
||||
if (i.guid == guid) {
|
||||
i.name = name;
|
||||
i.order = g_lastOrder;
|
||||
return;
|
||||
}
|
||||
}
|
||||
g_interactions.push_back({guid, name, g_lastOrder});
|
||||
}
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
// Retail's help lines and category (Ketho's CVars.lua dump).
|
||||
g_resortOnRecency = CVar::Factory::Register(
|
||||
"autoCompleteResortNamesOnRecency",
|
||||
"Shows people you recently spoke with higher up on the AutoComplete list.",
|
||||
Offsets::CVAR_CATEGORY_GAME, "1");
|
||||
g_editFromCenter = CVar::Factory::Register(
|
||||
"autoCompleteWhenEditingFromCenter",
|
||||
"If you edit a name by inserting characters into the center, a smarter auto-complete "
|
||||
"will occur.",
|
||||
Offsets::CVAR_CATEGORY_GAME, "1");
|
||||
g_useContext = CVar::Factory::Register(
|
||||
"autoCompleteUseContext",
|
||||
"The system will, for example, only show people in your guild when you are typing "
|
||||
"/gpromote. Names will also never be removed.",
|
||||
Offsets::CVAR_CATEGORY_GAME, "1");
|
||||
|
||||
Game::Lua::RegisterTableFunction("C_AutoComplete", "GetAutoCompleteResults",
|
||||
&Script_GetAutoCompleteResults, &kGetAutoCompleteResults);
|
||||
Game::Lua::RegisterTableFunction("C_AutoComplete", "IsRecognizedName", &Script_IsRecognizedName,
|
||||
&kIsRecognizedName);
|
||||
Game::Lua::RegisterIntegerEnum(
|
||||
"Enum", "AutoCompleteEntryFlag", kEntryFlagEntries, "AutoComplete");
|
||||
Game::Lua::RegisterIntegerEnum(
|
||||
"Enum", "AutoCompletePriority", kPriorityEntries, "AutoComplete");
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace AutoComplete::Names
|
||||
@@ -0,0 +1,27 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace AutoComplete::Names {
|
||||
|
||||
// Chat::Dispatch calls this for every dispatched chat line, before the engine
|
||||
// fires its CHAT_MSG_* event. A whisper to or from a player (`guid` is the
|
||||
// other player: Chat::Dispatch's sender) makes that player an InteractedWith
|
||||
// name, most recent first — 4.3.4's ChatFrame does the same in its message
|
||||
// handler. Every other line is ignored.
|
||||
void OnChat(int chatType, int languageID, const char *name, uint64_t guid);
|
||||
|
||||
} // namespace AutoComplete::Names
|
||||
@@ -19,8 +19,9 @@
|
||||
// the `BAG_UPDATE_DELAYED` event. Subscribe here to run C++ work exactly once
|
||||
// per frame in which any bag actually changed, instead of polling every frame.
|
||||
//
|
||||
// Callbacks fire at the tail of the world tick, after every BAG_UPDATE for the
|
||||
// frame has already fired (so bag contents are settled). No firing on frames
|
||||
// Callbacks fire from the world tick's subscription, after the frame's UI draw
|
||||
// and after every BAG_UPDATE for the frame has already fired (so bag contents
|
||||
// are settled). No firing on frames
|
||||
// where nothing changed. Order across subscribers is unspecified.
|
||||
//
|
||||
// Module pattern, matching `Tick::WorldTick::AutoSubscribe`: declare a
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `debuglocals([level [, skipFunctionsAndUserdata]])` — the locals and then
|
||||
// the upvalues of a call level as text, one `name=value` line each, for an
|
||||
// error report. 4.3.4's Script_debuglocals and the FrameScript_PackVariable
|
||||
// it formats with, in retail's spacing (probed: `n=7`, `s="hi"`,
|
||||
// `t=<table>{`, ` k=1`, `}`; 4.3.4 writes `n = 7` and `t = <table> {`):
|
||||
// - Only while an error handler runs; otherwise it returns nothing. 4.3.4
|
||||
// tests a flag its error function sets around the handler call. 1.12's
|
||||
// error function sets none, but while a handler runs that function's
|
||||
// level is on the stack (ErrorCallstack::ErrorFunctionLevel).
|
||||
// - `level` counts as lua_getstack and debugstack do: 1 is the caller.
|
||||
// Less than 1 is 1.
|
||||
// - A table prints as `name=<table>{`, or with the frame's name for a
|
||||
// frame (t[0], as FrameScript_GetObject reads it), then its fields one
|
||||
// space in at the top level only, then `}`. A function prints where it
|
||||
// is defined. The text stops at 4095 bytes, each line at 1023.
|
||||
// Retail's second argument, which its Blizzard_ScriptErrors passes as true,
|
||||
// leaves out the function and userdata values, at the top level and in
|
||||
// tables.
|
||||
//
|
||||
// A number key in int range prints truncated with %d, as 4.3.4's
|
||||
// IsIntegerNumber tests only the range: 2.5 prints as 2. Two departures from
|
||||
// 4.3.4, both where it would fail inside the handler: a number key outside
|
||||
// that range is formatted from a copy, not converted in place (that breaks
|
||||
// lua_next), and a key that is not a number or string prints as its type in
|
||||
// angle brackets (4.3.4 formats a null string).
|
||||
//
|
||||
// Retail also lists `(*temporary)` lines: 5.1's lua_getlocal names each stack
|
||||
// slot past a level's locals so, a C level's arguments included (an
|
||||
// `error("x")` report shows `(*temporary)="x"`). 1.12's lua_getlocal
|
||||
// (0x6FBB20) is 5.0's, which names declared locals only. Those slots are
|
||||
// the compiler's registers, which 5.0 and 5.1 allocate differently, so they
|
||||
// are left out rather than imitated.
|
||||
//
|
||||
// Registered on both Lua states, like the base-library helpers.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "baselib/ErrorCallstack.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace BaseLib::DebugLocals {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace Lua = Game::Lua;
|
||||
|
||||
using lua_getlocal_t = const char *(__fastcall *)(void *L, const void *ar, int n);
|
||||
using lua_getupvalue_t = const char *(__fastcall *)(void *L, int funcindex, int n);
|
||||
|
||||
const auto GetLocal = reinterpret_cast<lua_getlocal_t>(Offsets::LUA_GET_LOCAL);
|
||||
const auto GetUpvalue = reinterpret_cast<lua_getupvalue_t>(Offsets::LUA_GET_UPVALUE);
|
||||
|
||||
constexpr size_t kTextMax = 4095; // 4.3.4's 4096-byte buffer, less its NUL
|
||||
constexpr size_t kLineSize = 1024;
|
||||
|
||||
struct Packer {
|
||||
void *L;
|
||||
bool skipFunctionsAndUserdata;
|
||||
std::string text;
|
||||
|
||||
void Line(const char *fmt, const char *indent, const char *name, const char *value = "",
|
||||
int number = 0) {
|
||||
char line[kLineSize];
|
||||
std::snprintf(line, sizeof line, fmt, indent, name, value, number);
|
||||
const size_t room = kTextMax - text.size();
|
||||
text.append(line, std::min(room, std::strlen(line)));
|
||||
}
|
||||
|
||||
// The value on the stack top, as 4.3.4's FrameScript_PackVariable.
|
||||
void Pack(const char *name, int depth) {
|
||||
const char *indent = depth > 0 ? " " : "";
|
||||
switch (Lua::Type(L, -1)) {
|
||||
case Lua::TYPE_NIL:
|
||||
Line("%s%s=nil\n", indent, name);
|
||||
break;
|
||||
case Lua::TYPE_BOOLEAN:
|
||||
Line("%s%s=%s\n", indent, name, Lua::ToBoolean(L, -1) ? "true" : "false");
|
||||
break;
|
||||
case Lua::TYPE_LIGHTUSERDATA:
|
||||
case Lua::TYPE_USERDATA:
|
||||
if (!skipFunctionsAndUserdata)
|
||||
Line("%s%s=<userdata>\n", indent, name);
|
||||
break;
|
||||
case Lua::TYPE_NUMBER:
|
||||
Line("%s%s=%s\n", indent, name, Lua::ToString(L, -1)); // a copy: safe to convert
|
||||
break;
|
||||
case Lua::TYPE_STRING:
|
||||
Line("%s%s=\"%s\"\n", indent, name, Lua::ToString(L, -1));
|
||||
break;
|
||||
case Lua::TYPE_TABLE:
|
||||
PackTable(name, depth, indent);
|
||||
break;
|
||||
case Lua::TYPE_FUNCTION:
|
||||
if (!skipFunctionsAndUserdata)
|
||||
PackFunction(name, indent);
|
||||
break;
|
||||
default:
|
||||
Line("%s%s=<%s>\n", indent, name, Lua::TypeName(L, Lua::Type(L, -1)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void PackTable(const char *name, int depth, const char *indent) {
|
||||
void *object = Lua::ResolveObject(L, -1);
|
||||
Line("%s%s=%s{\n", indent, name, object ? Lua::ObjectDisplayName(object) : "<table>");
|
||||
if (depth == 0) {
|
||||
Lua::PushNil(L);
|
||||
while (Lua::Next(L, -2) != 0) {
|
||||
char key[32];
|
||||
const char *keyName = key;
|
||||
const int keyType = Lua::Type(L, -2);
|
||||
const double number = keyType == Lua::TYPE_NUMBER ? Lua::ToNumber(L, -2) : 0;
|
||||
if (keyType == Lua::TYPE_NUMBER && std::isfinite(number) && number >= -2147483648.0 &&
|
||||
number <= 2147483647.0) {
|
||||
std::snprintf(key, sizeof key, "%d", static_cast<int>(number));
|
||||
} else if (keyType == Lua::TYPE_STRING) {
|
||||
keyName = Lua::ToString(L, -2);
|
||||
} else if (keyType == Lua::TYPE_NUMBER) {
|
||||
Lua::PushValue(L, -2);
|
||||
std::snprintf(key, sizeof key, "%s", Lua::ToString(L, -1));
|
||||
Lua::SetTop(L, -2);
|
||||
} else {
|
||||
std::snprintf(key, sizeof key, "<%s>", Lua::TypeName(L, keyType));
|
||||
}
|
||||
Pack(keyName, 1);
|
||||
Lua::SetTop(L, -2); // the value; the key stays for lua_next
|
||||
}
|
||||
}
|
||||
Line("%s}\n", indent, "");
|
||||
}
|
||||
|
||||
void PackFunction(const char *name, const char *indent) {
|
||||
alignas(4) uint8_t ar[Offsets::SIZEOF_LUA_DEBUG];
|
||||
Lua::PushValue(L, -1);
|
||||
Lua::GetInfo(L, ">nS", ar); // pops the copy
|
||||
const auto field = [&ar](uintptr_t offset) { return Game::Read<const char *>(ar, offset); };
|
||||
const char *source = field(Offsets::OFF_LUA_DEBUG_SOURCE);
|
||||
const int line = Game::Read<int>(ar, Offsets::OFF_LUA_DEBUG_LINEDEFINED);
|
||||
if (const char *fn = field(Offsets::OFF_LUA_DEBUG_NAME)) {
|
||||
char named[kLineSize];
|
||||
std::snprintf(named, sizeof named, "%s() defined %s", fn, source ? source : "?");
|
||||
Line("%s%s=%s:%d\n", indent, name, named, line);
|
||||
} else {
|
||||
Line("%s%s=<function> defined %s:%d\n", indent, name, source ? source : "?", line);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
int __fastcall Script_debuglocals(void *L) {
|
||||
if (ErrorCallstack::ErrorFunctionLevel(L) == 0)
|
||||
return 0;
|
||||
int level = static_cast<int>(Lua::ToNumber(L, 1));
|
||||
if (level <= 0)
|
||||
level = 1;
|
||||
Packer packer{L, Lua::ToBoolean(L, 2) != 0, {}};
|
||||
|
||||
alignas(4) uint8_t ar[Offsets::SIZEOF_LUA_DEBUG];
|
||||
if (Lua::GetStack(L, level, ar)) {
|
||||
const char *name;
|
||||
for (int i = 1; (name = GetLocal(L, ar, i)) != nullptr; i++) {
|
||||
packer.Pack(name, 0);
|
||||
Lua::SetTop(L, -2);
|
||||
}
|
||||
Lua::GetInfo(L, "f", ar); // the level's function
|
||||
for (int i = 1; (name = GetUpvalue(L, -1, i)) != nullptr; i++) {
|
||||
packer.Pack(name, 0);
|
||||
Lua::SetTop(L, -2);
|
||||
}
|
||||
Lua::SetTop(L, -2);
|
||||
}
|
||||
Lua::PushLString(L, packer.text.data(), static_cast<unsigned int>(packer.text.size()));
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kDebuglocalsArgs[] = {
|
||||
Game::Doc::Opt("level", "number", "1", "The call level, as debugstack counts it."),
|
||||
Game::Doc::Opt("skipFunctionsAndUserdata", "boolean", "false",
|
||||
"Leaves out function and userdata values."),
|
||||
};
|
||||
const Game::Doc::Field kDebuglocalsRets[] = {
|
||||
Game::Doc::Opt("locals", "string", nullptr,
|
||||
"One `name=value` line for each local and upvalue. Nothing outside an "
|
||||
"error handler."),
|
||||
};
|
||||
const Game::Doc::Function kDebuglocals{
|
||||
"The local variables and upvalues of a call level as text, for an error report.",
|
||||
kDebuglocalsArgs, kDebuglocalsRets, "LuaGlobals"};
|
||||
|
||||
void RegisterInGame() {
|
||||
Game::Lua::RegisterGlobalFunction("debuglocals", &Script_debuglocals, &kDebuglocals);
|
||||
}
|
||||
|
||||
void RegisterGlue() {
|
||||
Game::Lua::RegisterGlueFunction("debuglocals", &Script_debuglocals, &kDebuglocals);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterInGame};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterGlue};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::DebugLocals
|
||||
@@ -0,0 +1,102 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `debugstack([thread,] [start [, count1 [, count2]]])` — the stack of a
|
||||
// coroutine, as 4.3.4's Script_debugstack takes it: with a thread first,
|
||||
// the levels are that thread's and `start` defaults to 0 (the thread's
|
||||
// innermost level) instead of 1.
|
||||
//
|
||||
// 1.12's debugstack (FUN_SCRIPT_DEBUGSTACK) has no thread argument, and a
|
||||
// thread falls through its argument checks so that it returns the thread
|
||||
// itself: `msg .. debugstack(co)` raises "attempt to concatenate a thread
|
||||
// value" in the coroutine error paths of WeakAuras, Questie's ThreadLib,
|
||||
// MRT and Zygor.
|
||||
//
|
||||
// The engine's function still does the work, so its text and its head and
|
||||
// tail windows stay exactly as they are. It reads levels through six calls
|
||||
// (five lua_getstack, one lua_getinfo), and those calls are redirected to
|
||||
// read from the state Script_debugstack names for the call: the thread, or
|
||||
// the calling state when there is none. Script_debugstack replaces the
|
||||
// global and is the engine function's only way in, so the state is set for
|
||||
// every call; an error part way cannot leave a thread behind for the next.
|
||||
//
|
||||
// Registered on both Lua states, like the base-library helpers.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <initializer_list>
|
||||
|
||||
namespace BaseLib::DebugStack {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace Lua = Game::Lua;
|
||||
|
||||
// The state whose levels the engine's debugstack reads, or null for its own.
|
||||
void *g_levelsOf = nullptr;
|
||||
|
||||
int __fastcall GetStack(void *L, int level, void *ar) {
|
||||
return Lua::GetStack(g_levelsOf ? g_levelsOf : L, level, ar);
|
||||
}
|
||||
|
||||
int __fastcall GetInfo(void *L, const char *what, void *ar) {
|
||||
return Lua::GetInfo(g_levelsOf ? g_levelsOf : L, what, ar);
|
||||
}
|
||||
|
||||
int __fastcall Script_debugstack(void *L) {
|
||||
const auto engineDebugStack =
|
||||
reinterpret_cast<Lua::CFunction>(Offsets::FUN_SCRIPT_DEBUGSTACK);
|
||||
g_levelsOf = nullptr;
|
||||
if (Lua::Type(L, 1) == Lua::TYPE_THREAD) {
|
||||
g_levelsOf = Lua::ToThread(L, 1);
|
||||
Lua::Remove(L, 1);
|
||||
if (!Lua::IsNumber(L, 1)) { // start defaults to 0 for a thread
|
||||
Lua::PushNumber(L, 0);
|
||||
Lua::Insert(L, 1);
|
||||
}
|
||||
}
|
||||
const int results = engineDebugStack(L);
|
||||
g_levelsOf = nullptr;
|
||||
return results;
|
||||
}
|
||||
|
||||
void Apply() {
|
||||
const auto getStack = reinterpret_cast<const void *>(&GetStack);
|
||||
for (const uintptr_t site :
|
||||
{Offsets::CALL_DEBUGSTACK_GETSTACK_FIRST, Offsets::CALL_DEBUGSTACK_GETSTACK_TAIL_PROBE,
|
||||
Offsets::CALL_DEBUGSTACK_GETSTACK_TAIL_START, Offsets::CALL_DEBUGSTACK_GETSTACK_TAIL_SKIP,
|
||||
Offsets::CALL_DEBUGSTACK_GETSTACK_NEXT})
|
||||
Common::RedirectCall(site, Offsets::LUA_GET_STACK, getStack);
|
||||
Common::RedirectCall(Offsets::CALL_DEBUGSTACK_GETINFO, Offsets::LUA_GET_INFO,
|
||||
reinterpret_cast<const void *>(&GetInfo));
|
||||
}
|
||||
|
||||
void RegisterInGame() {
|
||||
Apply();
|
||||
Lua::RegisterGlobalFunction("debugstack", &Script_debugstack);
|
||||
}
|
||||
|
||||
void RegisterGlue() {
|
||||
Apply();
|
||||
Lua::RegisterGlueFunction("debugstack", &Script_debugstack);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterInGame};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterGlue};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::DebugStack
|
||||
@@ -0,0 +1,58 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `difftime(t2 [, t1])` — the seconds from `t1` (0 by default) to `t2`, the
|
||||
// companion of the engine's `date` and `time` globals. 4.3.4's
|
||||
// Script_difftime: C `difftime` over the two arguments as time_t.
|
||||
//
|
||||
// Registered on BOTH states, as the engine registers `date` and `time`.
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
#include <ctime>
|
||||
|
||||
namespace BaseLib::DiffTime {
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_difftime(void *L) {
|
||||
const auto t2 = static_cast<time_t>(Game::Lua::CheckNumber(L, 1));
|
||||
const auto t1 = static_cast<time_t>(Game::Lua::OptNumber(L, 2, 0));
|
||||
Game::Lua::PushNumber(L, std::difftime(t2, t1));
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kDifftimeArgs[] = {
|
||||
Game::Doc::Req("time2", "number", "A time in seconds, as `time()` returns."),
|
||||
Game::Doc::Opt("time1", "number", "0"),
|
||||
};
|
||||
const Game::Doc::Field kDifftimeRets[] = {
|
||||
Game::Doc::Req("seconds", "number", "time2 minus time1."),
|
||||
};
|
||||
const Game::Doc::Function kDifftime{"The number of seconds from time1 to time2.",
|
||||
kDifftimeArgs, kDifftimeRets, "LuaGlobals"};
|
||||
|
||||
void RegisterInGame() {
|
||||
Game::Lua::RegisterGlobalFunction("difftime", &Script_difftime, &kDifftime);
|
||||
}
|
||||
|
||||
void RegisterGlue() {
|
||||
Game::Lua::RegisterGlueFunction("difftime", &Script_difftime, &kDifftime);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterInGame};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterGlue};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::DiffTime
|
||||
@@ -0,0 +1,145 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `GetCallstackHeight()`, `GetErrorCallstackHeight()` and
|
||||
// `SetErrorCallstackHeight([height])` — retail's call stack heights. Its
|
||||
// error handler turns them into the debugstack level of the code that
|
||||
// failed, however many functions sit between that code and the handler:
|
||||
// debugstack(GetCallstackHeight() - (GetErrorCallstackHeight() - 1))
|
||||
// A height counts call levels as lua_getstack does, and so as debugstack
|
||||
// does: C functions and lost tail calls count, and level 1 is the caller of
|
||||
// the C function that asks.
|
||||
//
|
||||
// 1.12 records no error height, and needs none. Every error report runs the
|
||||
// engine's error function (FUN_FRAMESCRIPT_CALL_ERROR_HANDLER) as a C
|
||||
// closure: each script pcall passes it as the errfunc, so Lua calls it at
|
||||
// the point of the error before the stack unwinds, and a chunk that fails
|
||||
// to compile is reported by a pcall of it (FUN_LUA_RUN_STRING). It calls the
|
||||
// seterrorhandler function with lua_call. So while any handler runs, that
|
||||
// closure's level is on the stack directly above the code that failed, and
|
||||
// GetErrorCallstackHeight answers the height below the innermost one.
|
||||
// Nothing is recorded, so a handler that fails part way leaves nothing
|
||||
// behind, and a nested error finds its own level.
|
||||
//
|
||||
// SetErrorCallstackHeight is for a handler called directly, with no error
|
||||
// raised (retail's CallErrorHandler and assertsafe): with no engine level on
|
||||
// the stack, GetErrorCallstackHeight answers the value set. An engine level
|
||||
// wins over it, so a value a failed handler left set does not move the
|
||||
// stack of a later error. Retail's order between the two is not known; no
|
||||
// retail code sets the value around a raised error.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "baselib/ErrorCallstack.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
namespace BaseLib::ErrorCallstack {
|
||||
|
||||
int ErrorFunctionLevel(void *L) {
|
||||
const auto errorFunction = reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_FRAMESCRIPT_CALL_ERROR_HANDLER);
|
||||
alignas(4) uint8_t ar[Offsets::SIZEOF_LUA_DEBUG];
|
||||
for (int level = 1; Game::Lua::GetStack(L, level, ar); ++level) {
|
||||
Game::Lua::GetInfo(L, "f", ar); // the level's function; nil for a lost tail call
|
||||
const bool found = Game::Lua::ToCFunction(L, -1) == errorFunction;
|
||||
Game::Lua::SetTop(L, -2);
|
||||
if (found)
|
||||
return level;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
std::optional<int> g_setHeight;
|
||||
|
||||
// The number of call levels below the running C function.
|
||||
int Height(void *L) {
|
||||
alignas(4) uint8_t ar[Offsets::SIZEOF_LUA_DEBUG];
|
||||
int height = 0;
|
||||
while (Game::Lua::GetStack(L, height + 1, ar))
|
||||
++height;
|
||||
return height;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetCallstackHeight(void *L) {
|
||||
Game::Lua::PushNumber(L, Height(L));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetErrorCallstackHeight(void *L) {
|
||||
const int level = ErrorFunctionLevel(L);
|
||||
if (level > 0)
|
||||
Game::Lua::PushNumber(L, Height(L) - level);
|
||||
else if (g_setHeight)
|
||||
Game::Lua::PushNumber(L, *g_setHeight);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetErrorCallstackHeight(void *L) {
|
||||
if (Game::Lua::Type(L, 1) <= Game::Lua::TYPE_NIL)
|
||||
g_setHeight.reset();
|
||||
else if (Game::Lua::IsNumber(L, 1))
|
||||
g_setHeight = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
else
|
||||
Game::Lua::Error(L, "Usage: SetErrorCallstackHeight([height])");
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kHeightRets[] = {
|
||||
Game::Doc::Req("height", "number", "The calling function's level and every level below it."),
|
||||
};
|
||||
const Game::Doc::Field kErrorHeightRets[] = {
|
||||
Game::Doc::Opt("height", "number", nullptr, "The stack height of the code that caused the error."),
|
||||
};
|
||||
const Game::Doc::Field kSetArgs[] = {
|
||||
Game::Doc::Opt("height", "number", nullptr, "The height to return, or nil to clear it."),
|
||||
};
|
||||
const Game::Doc::Function kGetCallstackHeight{
|
||||
"Returns the number of call levels on the stack, counting the calling function as level 1.",
|
||||
{}, kHeightRets, "FrameScriptGlobals"};
|
||||
const Game::Doc::Function kGetErrorCallstackHeight{
|
||||
"Returns the call stack height of the code that caused the error being handled, or nil when no error is being handled.",
|
||||
{}, kErrorHeightRets, "FrameScriptGlobals"};
|
||||
const Game::Doc::Function kSetErrorCallstackHeight{
|
||||
"Sets the height that GetErrorCallstackHeight returns for an error handler called with no error raised.",
|
||||
kSetArgs, {}, "FrameScriptGlobals"};
|
||||
|
||||
// A new Lua state starts with no height set.
|
||||
void RegisterInGame() {
|
||||
g_setHeight.reset();
|
||||
Game::Lua::RegisterGlobalFunction("GetCallstackHeight", &Script_GetCallstackHeight, &kGetCallstackHeight);
|
||||
Game::Lua::RegisterGlobalFunction("GetErrorCallstackHeight", &Script_GetErrorCallstackHeight,
|
||||
&kGetErrorCallstackHeight);
|
||||
Game::Lua::RegisterGlobalFunction("SetErrorCallstackHeight", &Script_SetErrorCallstackHeight,
|
||||
&kSetErrorCallstackHeight);
|
||||
}
|
||||
|
||||
void RegisterGlue() {
|
||||
g_setHeight.reset();
|
||||
Game::Lua::RegisterGlueFunction("GetCallstackHeight", &Script_GetCallstackHeight, &kGetCallstackHeight);
|
||||
Game::Lua::RegisterGlueFunction("GetErrorCallstackHeight", &Script_GetErrorCallstackHeight,
|
||||
&kGetErrorCallstackHeight);
|
||||
Game::Lua::RegisterGlueFunction("SetErrorCallstackHeight", &Script_SetErrorCallstackHeight,
|
||||
&kSetErrorCallstackHeight);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterInGame};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterGlue};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::ErrorCallstack
|
||||
@@ -0,0 +1,23 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace BaseLib::ErrorCallstack {
|
||||
|
||||
// The innermost call level whose function is the engine's error function
|
||||
// (FUN_FRAMESCRIPT_CALL_ERROR_HANDLER), or 0 when no error is being
|
||||
// reported. Nonzero exactly while an error handler runs.
|
||||
int ErrorFunctionLevel(void *L);
|
||||
|
||||
} // namespace BaseLib::ErrorCallstack
|
||||
+22
-1
@@ -16,13 +16,16 @@
|
||||
// - `math.fmod(x, y)` — floating-point remainder of `x / y`.
|
||||
// - `math.modf(x)` — split into integral + fractional parts.
|
||||
// - `math.huge` — positive infinity constant.
|
||||
// - `math.cosh(x)`, `math.sinh(x)`, `math.tanh(x)` — hyperbolic functions.
|
||||
//
|
||||
// `math.fmod` is a pure rename: Lua 5.0 ships the exact same C function under
|
||||
// the name `math.mod`; 5.1 renamed the Lua-facing entry to `math.fmod`. We
|
||||
// register it as a direct alias of the engine's `math.mod` C function
|
||||
// (`FUN_LUA_MATH_FMOD`) — no wrapper. `math.mod` stays available too, so code
|
||||
// written for either name works. `modf` and `huge` are genuinely new in 5.1
|
||||
// (not in 1.12's mathlib table), so we implement/define them here.
|
||||
// (not in 1.12's mathlib table), so we implement/define them here, as are the
|
||||
// hyperbolic functions (5.1's math_cosh/sinh/tanh: luaL_checknumber, the C
|
||||
// function, one number).
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
@@ -53,9 +56,27 @@ int __fastcall Script_math_modf(void *L) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
int __fastcall Script_math_cosh(void *L) {
|
||||
Game::Lua::PushNumber(L, std::cosh(Game::Lua::CheckNumber(L, 1)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_math_sinh(void *L) {
|
||||
Game::Lua::PushNumber(L, std::sinh(Game::Lua::CheckNumber(L, 1)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_math_tanh(void *L) {
|
||||
Game::Lua::PushNumber(L, std::tanh(Game::Lua::CheckNumber(L, 1)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterFns() {
|
||||
Game::Lua::RegisterTableFunction("math", "fmod", Script_math_fmod);
|
||||
Game::Lua::RegisterTableFunction("math", "modf", &Script_math_modf);
|
||||
Game::Lua::RegisterTableFunction("math", "cosh", &Script_math_cosh);
|
||||
Game::Lua::RegisterTableFunction("math", "sinh", &Script_math_sinh);
|
||||
Game::Lua::RegisterTableFunction("math", "tanh", &Script_math_tanh);
|
||||
|
||||
// `math.huge` — a numeric constant, not a function. Fetch the (now
|
||||
// guaranteed-present) `math` table and set the field directly.
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `newproxy([false | true | proxy])` — Lua 5.1's empty userdata (luaB_newproxy,
|
||||
// lbaselib.c). `false` or nothing: no metatable. `true`: a new metatable of
|
||||
// its own. Another proxy: share that proxy's metatable. Only metatables made
|
||||
// by `newproxy(true)` count; any other argument raises "boolean or proxy
|
||||
// expected". 5.0 has userdata and their `__gc`, only not this function.
|
||||
//
|
||||
// As in 5.1, the closure's upvalue is a weak table that marks the valid
|
||||
// metatables (weak[m] = true), and is its own metatable with __mode "kv".
|
||||
//
|
||||
// Registered on BOTH states (language-level helper, both are 5.0).
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
namespace BaseLib::NewProxy {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace Lua = Game::Lua;
|
||||
|
||||
int __fastcall Script_newproxy(void *L) {
|
||||
Lua::SetTop(L, 1);
|
||||
Lua::NewUserdata(L, 0); // the proxy, at 2
|
||||
if (Lua::ToBoolean(L, 1) == 0)
|
||||
return 1; // no metatable
|
||||
if (Lua::Type(L, 1) == Lua::TYPE_BOOLEAN) {
|
||||
Lua::NewTable(L); // a new metatable m ...
|
||||
Lua::PushValue(L, -1);
|
||||
Lua::PushBoolean(L, 1);
|
||||
Lua::RawSet(L, Lua::UpvalueIndex(1)); // ... marked valid: weak[m] = true
|
||||
} else {
|
||||
bool valid = false;
|
||||
if (Lua::GetMetatable(L, 1)) {
|
||||
Lua::RawGet(L, Lua::UpvalueIndex(1));
|
||||
valid = Lua::ToBoolean(L, -1) != 0;
|
||||
Lua::SetTop(L, -2);
|
||||
}
|
||||
if (!valid)
|
||||
Lua::ArgError(L, 1, "boolean or proxy expected");
|
||||
Lua::GetMetatable(L, 1);
|
||||
}
|
||||
Lua::SetMetatable(L, 2);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void Register() {
|
||||
void *L = Lua::State();
|
||||
if (L == nullptr)
|
||||
return;
|
||||
Lua::PushString(L, "newproxy");
|
||||
Lua::NewTable(L); // the weak table w
|
||||
Lua::PushString(L, "__mode");
|
||||
Lua::PushString(L, "kv");
|
||||
Lua::RawSet(L, -3);
|
||||
Lua::PushValue(L, -1);
|
||||
Lua::SetMetatable(L, -2); // w is its own metatable
|
||||
Lua::PushCClosure(L, &Script_newproxy, 1);
|
||||
Lua::SetTable(L, Lua::GLOBALS_INDEX);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Register};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&Register};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::NewProxy
|
||||
@@ -0,0 +1,63 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `scrub(...)` — returns its arguments, each one that is not a boolean,
|
||||
// number or string replaced by nil. 4.3.4's Script_scrub.
|
||||
//
|
||||
// Registered on both Lua states, like the base-library helpers.
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
namespace BaseLib::Scrub {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace Lua = Game::Lua;
|
||||
|
||||
int __fastcall Script_scrub(void *L) {
|
||||
const int top = Lua::GetTop(L);
|
||||
Lua::CheckStack(L, 1);
|
||||
for (int i = 1; i <= top; i++) {
|
||||
const int type = Lua::Type(L, i);
|
||||
if (type != Lua::TYPE_BOOLEAN && type != Lua::TYPE_NUMBER && type != Lua::TYPE_STRING) {
|
||||
Lua::PushNil(L);
|
||||
Lua::Replace(L, i);
|
||||
}
|
||||
}
|
||||
return top;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kScrubArgs[] = {
|
||||
Game::Doc::Vararg("any"),
|
||||
};
|
||||
const Game::Doc::Field kScrubRets[] = {
|
||||
Game::Doc::Vararg("any", "Each argument, or nil when it is not a boolean, number or string."),
|
||||
};
|
||||
const Game::Doc::Function kScrub{
|
||||
"Returns its arguments, with each one that is not a boolean, number or string replaced by nil.",
|
||||
kScrubArgs, kScrubRets, "LuaGlobals"};
|
||||
|
||||
void RegisterInGame() {
|
||||
Game::Lua::RegisterGlobalFunction("scrub", &Script_scrub, &kScrub);
|
||||
}
|
||||
|
||||
void RegisterGlue() {
|
||||
Game::Lua::RegisterGlueFunction("scrub", &Script_scrub, &kScrub);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterInGame};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterGlue};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::Scrub
|
||||
@@ -11,7 +11,8 @@
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `securecall(func or name, ...)`, `securecallfunction(func, ...)` and
|
||||
// `securecall(func or name, ...)`, `securecallfunction(func, ...)`,
|
||||
// `securecallmethod(object, method, ...)` and
|
||||
// `secureexecuterange(tbl, func, ...)` — the later clients' protected calls,
|
||||
// less taint (1.12 has none). Retail, probed:
|
||||
// - securecall returns the function's results; a string names a global
|
||||
@@ -22,6 +23,11 @@
|
||||
// - secureexecuterange calls func(key, value, ...) for each entry of tbl in
|
||||
// `next` order; an error in one goes to the error handler and the walk
|
||||
// goes on. It returns nothing.
|
||||
// securecallmethod is from retail's API documentation, not a probe: it calls
|
||||
// object[method](object, ...) with a raw lookup (no metatable), and an error
|
||||
// in the lookup or the call goes to the error handler with no results. Here
|
||||
// a lookup that finds no function, or an object that is not a table, errors
|
||||
// the way calling that value does.
|
||||
// The error handler is the one every engine script run passes
|
||||
// (VAR_FRAMESCRIPT_ERROR_HANDLER_REF), as HookScript's wrapper uses.
|
||||
//
|
||||
@@ -70,6 +76,20 @@ int __fastcall Script_securecall(void *L) {
|
||||
|
||||
int __fastcall Script_securecallfunction(void *L) { return CallProtected(L); }
|
||||
|
||||
int __fastcall Script_securecallmethod(void *L) {
|
||||
if (Game::Lua::GetTop(L) < 2)
|
||||
Game::Lua::SetTop(L, 2);
|
||||
if (Game::Lua::Type(L, 1) == Game::Lua::TYPE_TABLE) {
|
||||
Game::Lua::PushValue(L, 2);
|
||||
Game::Lua::RawGet(L, 1);
|
||||
} else {
|
||||
Game::Lua::PushNil(L);
|
||||
}
|
||||
Game::Lua::Remove(L, 2); // [object, args..., func]
|
||||
Game::Lua::Insert(L, 1); // [func, object, args...]
|
||||
return CallProtected(L);
|
||||
}
|
||||
|
||||
int __fastcall Script_secureexecuterange(void *L) {
|
||||
const int n = Game::Lua::GetTop(L); // tbl, func, extra args 3..n
|
||||
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE)
|
||||
@@ -97,6 +117,11 @@ const Game::Doc::Field kCallArgs[] = {
|
||||
const Game::Doc::Field kCallRets[] = {
|
||||
Game::Doc::Opt("...", "any", nullptr, "The function's results, or nothing after an error."),
|
||||
};
|
||||
const Game::Doc::Field kMethodArgs[] = {
|
||||
Game::Doc::Req("object", "table", "The table that holds the method. The lookup ignores its metatable."),
|
||||
Game::Doc::Req("method", "string", "The name of the method."),
|
||||
Game::Doc::Opt("...", "any", nullptr, "The arguments after object."),
|
||||
};
|
||||
const Game::Doc::Field kRangeArgs[] = {
|
||||
Game::Doc::Req("tbl", "table", "The table whose entries are visited."),
|
||||
Game::Doc::Req("func", "function", "Called as func(key, value, ...) for each entry."),
|
||||
@@ -108,6 +133,9 @@ const Game::Doc::Function kSecureCall{
|
||||
const Game::Doc::Function kSecureCallFunction{
|
||||
"Calls a function and returns its results. An error goes to the error handler and the call returns nothing.",
|
||||
kCallArgs, kCallRets, "LuaGlobals"};
|
||||
const Game::Doc::Function kSecureCallMethod{
|
||||
"Calls object[method](object, ...) and returns its results. An error goes to the error handler and the call returns nothing.",
|
||||
kMethodArgs, kCallRets, "LuaGlobals"};
|
||||
const Game::Doc::Function kSecureExecuteRange{
|
||||
"Calls a function for each entry of a table. An error goes to the error handler and the other entries still run.",
|
||||
kRangeArgs, {}, "LuaGlobals"};
|
||||
@@ -115,12 +143,14 @@ const Game::Doc::Function kSecureExecuteRange{
|
||||
void RegisterInGame() {
|
||||
Game::Lua::RegisterGlobalFunction("securecall", &Script_securecall, &kSecureCall);
|
||||
Game::Lua::RegisterGlobalFunction("securecallfunction", &Script_securecallfunction, &kSecureCallFunction);
|
||||
Game::Lua::RegisterGlobalFunction("securecallmethod", &Script_securecallmethod, &kSecureCallMethod);
|
||||
Game::Lua::RegisterGlobalFunction("secureexecuterange", &Script_secureexecuterange, &kSecureExecuteRange);
|
||||
}
|
||||
|
||||
void RegisterGlue() {
|
||||
Game::Lua::RegisterGlueFunction("securecall", &Script_securecall, &kSecureCall);
|
||||
Game::Lua::RegisterGlueFunction("securecallfunction", &Script_securecallfunction, &kSecureCallFunction);
|
||||
Game::Lua::RegisterGlueFunction("securecallmethod", &Script_securecallmethod, &kSecureCallMethod);
|
||||
Game::Lua::RegisterGlueFunction("secureexecuterange", &Script_secureexecuterange, &kSecureExecuteRange);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `string.format` takes two-digit argument numbers (`%10$s`).
|
||||
//
|
||||
// 1.12's str_format (0x7FD3D0) is Lua 4.0's: a spec may start with `n$`, but
|
||||
// only one digit, `isdigit(p[0]) && p[1] == '$'`. In "%10$s" the "10" is then
|
||||
// read as a width and the '$' as the conversion, which raises "invalid
|
||||
// option in `format'". 4.3.4's str_format adds a second branch for two
|
||||
// digits, `isdigit(p[1]) && p[2] == '$'`, and its combat log strings need it
|
||||
// (ACTION_SPELL_PERIODIC_LEECH_RESULT is "(%10$s %8$s Gained)").
|
||||
//
|
||||
// The thunk below stands in for the engine's one-digit branch and adds the
|
||||
// two-digit one. Everything else stays the engine's: its locale isdigit on
|
||||
// the first digit (already tested before the site), `arg++` after the site,
|
||||
// the flag/width/precision scan from EBX, and every conversion. Every caller
|
||||
// goes through it: `string.format`, `format`, and the C callers of
|
||||
// FUN_LUA_STR_FORMAT (Frame::FormattedText, Text::NumericFormatter).
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace BaseLib::StringFormat {
|
||||
|
||||
namespace {
|
||||
|
||||
static_assert(Offsets::LUA_STR_FORMAT_FRAME_ARG == 8, "ArgNumberThunk writes [ebp - 8]");
|
||||
|
||||
// ESI = the spec after '%', whose first byte is a digit. Sets the argument
|
||||
// number in [ebp - 8] and moves EBX past the `n$`; leaves both alone when
|
||||
// the spec has no `n$`.
|
||||
__declspec(naked) void ArgNumberThunk() {
|
||||
__asm {
|
||||
cmp byte ptr [esi + 1], '$'
|
||||
jne twoDigits
|
||||
movsx eax, byte ptr [esi]
|
||||
sub eax, '0'
|
||||
mov dword ptr [ebp - 8], eax
|
||||
lea ebx, [esi + 2]
|
||||
ret
|
||||
twoDigits:
|
||||
movzx eax, byte ptr [esi + 1]
|
||||
sub eax, '0'
|
||||
cmp eax, 9
|
||||
ja done
|
||||
cmp byte ptr [esi + 2], '$'
|
||||
jne done
|
||||
movsx ecx, byte ptr [esi]
|
||||
sub ecx, '0'
|
||||
lea ecx, [ecx + ecx * 4]
|
||||
lea eax, [eax + ecx * 2]
|
||||
mov dword ptr [ebp - 8], eax
|
||||
lea ebx, [esi + 3]
|
||||
done:
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
// Runs on every registration pass; a site already patched no longer matches
|
||||
// and is skipped.
|
||||
void Apply() {
|
||||
static const uint8_t kOneDigit[18] = {0x80, 0x7E, 0x01, 0x24, 0x75, 0x0C, 0x0F, 0xBE, 0x06,
|
||||
0x83, 0xE8, 0x30, 0x89, 0x45, 0xF8, 0x8D, 0x5E, 0x02};
|
||||
Common::ReplaceWithCall(Offsets::SITE_LUA_STR_FORMAT_ARG_NUMBER, kOneDigit, sizeof kOneDigit,
|
||||
reinterpret_cast<const void *>(&ArgNumberThunk));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Apply};
|
||||
const Game::GlueModuleAutoRegister _glueAutoreg{&Apply};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::StringFormat
|
||||
@@ -0,0 +1,163 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `string.gsub` takes 5.1's string and table replacements.
|
||||
//
|
||||
// 1.12's str_gsub (0x7FD0E0) is Lua 5.0's, and differs from 5.1 in its
|
||||
// replacement argument:
|
||||
// - it takes a string or a function, not a table;
|
||||
// - `%0` in a replacement string, and `%1` when the pattern has no
|
||||
// captures, raise "invalid capture index" (5.1: the whole match);
|
||||
// - a function that returns nil or false deletes the match (5.1 keeps it),
|
||||
// and one that returns a table deletes it too (5.1 raises an error).
|
||||
//
|
||||
// The first two only ever raised errors, so 5.1's behavior cannot break a
|
||||
// 1.12 addon. The third does: SuperAPI's CombatText_AddMessage returns nil
|
||||
// to delete the player's own " [0x...]" GUID from heal text. A function
|
||||
// replacement therefore stays the engine's own add_s.
|
||||
//
|
||||
// str_gsub itself stays: its match loop, anchor, count and buffer are the
|
||||
// same in both. Two parts are replaced:
|
||||
// - the argument gate (SITE_LUA_STR_GSUB_REPL_CHECK) becomes CheckReplacement;
|
||||
// - the one add_s call (CALL_LUA_STR_GSUB_ADD_S) goes to AddValue: 5.1's
|
||||
// add_s for a string or number, 5.1's table lookup for a table, and the
|
||||
// engine's add_s for a function. They use the engine's capture helpers.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace BaseLib::StringGsub {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace Lua = Game::Lua;
|
||||
|
||||
// Lua 5.0's MatchState, as str_gsub builds it on its stack (str_format's
|
||||
// spec scan reads the same layout: level at +0x0C, capture[0].len at +0x14).
|
||||
struct MatchState {
|
||||
const char *srcInit;
|
||||
const char *srcEnd;
|
||||
void *L;
|
||||
int level;
|
||||
struct {
|
||||
const char *init;
|
||||
int len;
|
||||
} capture[32];
|
||||
};
|
||||
|
||||
using add_s_t = void(__fastcall *)(MatchState *ms, void *b, const char *s, const char *e);
|
||||
using push_onecapture_t = void(__fastcall *)(MatchState *ms, int i);
|
||||
using luaL_addlstring_t = void(__fastcall *)(void *b, const char *s, unsigned int len);
|
||||
using luaL_addvalue_t = void(__fastcall *)(void *b);
|
||||
|
||||
const auto EngineAddS = reinterpret_cast<add_s_t>(Offsets::FUN_LUA_STR_ADD_S);
|
||||
const auto EnginePushOneCapture =
|
||||
reinterpret_cast<push_onecapture_t>(Offsets::FUN_LUA_STR_PUSH_ONECAPTURE);
|
||||
const auto AddLString = reinterpret_cast<luaL_addlstring_t>(Offsets::LUAL_ADD_LSTRING);
|
||||
const auto AddBufferValue = reinterpret_cast<luaL_addvalue_t>(Offsets::LUAL_ADD_VALUE);
|
||||
|
||||
// 5.1's push_onecapture: capture `i`, or the whole match for `i == 0` when
|
||||
// the pattern has no captures.
|
||||
void PushOneCapture(MatchState *ms, int i, const char *s, const char *e) {
|
||||
if (i < ms->level)
|
||||
EnginePushOneCapture(ms, i);
|
||||
else if (i == 0)
|
||||
Lua::PushLString(ms->L, s, static_cast<unsigned int>(e - s));
|
||||
else
|
||||
Lua::Error(ms->L, "invalid capture index");
|
||||
}
|
||||
|
||||
// 5.1's add_s: a replacement string, where `%0` is the whole match and `%1`
|
||||
// to `%9` are captures.
|
||||
void AddString(MatchState *ms, void *b, const char *s, const char *e) {
|
||||
void *L = ms->L;
|
||||
const char *news = Lua::ToString(L, 3);
|
||||
const unsigned int l = Lua::StrLen(L, 3);
|
||||
for (unsigned int i = 0; i < l; i++) {
|
||||
if (news[i] != '%') {
|
||||
AddLString(b, news + i, 1);
|
||||
continue;
|
||||
}
|
||||
i++; // skip the escape
|
||||
const char c = news[i];
|
||||
if (c < '0' || c > '9') {
|
||||
AddLString(b, news + i, 1);
|
||||
} else if (c == '0') {
|
||||
AddLString(b, s, static_cast<unsigned int>(e - s));
|
||||
} else {
|
||||
PushOneCapture(ms, c - '1', s, e);
|
||||
AddBufferValue(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5.1's add_value for a string or table, the engine's add_s for a function.
|
||||
// Called as add_s: __fastcall, ret 8.
|
||||
void __fastcall AddValue(MatchState *ms, void *b, const char *s, const char *e) {
|
||||
void *L = ms->L;
|
||||
switch (Lua::Type(L, 3)) {
|
||||
case Lua::TYPE_FUNCTION:
|
||||
// A result that is not a string or number deletes the match.
|
||||
EngineAddS(ms, b, s, e);
|
||||
return;
|
||||
case Lua::TYPE_TABLE:
|
||||
break;
|
||||
default: // number or string
|
||||
AddString(ms, b, s, e);
|
||||
return;
|
||||
}
|
||||
PushOneCapture(ms, 0, s, e);
|
||||
Lua::GetTable(L, 3);
|
||||
if (!Lua::ToBoolean(L, -1)) {
|
||||
Lua::SetTop(L, -2);
|
||||
Lua::PushLString(L, s, static_cast<unsigned int>(e - s)); // keep the match
|
||||
} else if (!Lua::IsString(L, -1)) {
|
||||
Lua::Error(L, "invalid replacement value (a %s)", Lua::TypeName(L, Lua::Type(L, -1)));
|
||||
}
|
||||
AddBufferValue(b);
|
||||
}
|
||||
|
||||
// 5.1's gate on the replacement argument. Called in place of the engine's,
|
||||
// with ECX = L.
|
||||
void __fastcall CheckReplacement(void *L) {
|
||||
const int t = Lua::Type(L, 3);
|
||||
if (t != Lua::TYPE_NUMBER && t != Lua::TYPE_STRING && t != Lua::TYPE_FUNCTION &&
|
||||
t != Lua::TYPE_TABLE)
|
||||
Lua::ArgError(L, 3, "string/function/table expected");
|
||||
}
|
||||
|
||||
// Runs on every registration pass; a site already patched no longer matches
|
||||
// and is skipped.
|
||||
void Apply() {
|
||||
static const uint8_t kGate[60] = {
|
||||
0xE8, 0x0D, 0x5F, 0xEF, 0xFF, 0x83, 0xF8, 0x03, 0x7C, 0x21, 0xBA, 0x03,
|
||||
0x00, 0x00, 0x00, 0x8B, 0xCF, 0xE8, 0x9C, 0x63, 0xEF, 0xFF, 0x85, 0xC0,
|
||||
0x75, 0x22, 0xBA, 0x03, 0x00, 0x00, 0x00, 0x8B, 0xCF, 0xE8, 0x7C, 0x62,
|
||||
0xEF, 0xFF, 0x83, 0xF8, 0x06, 0x74, 0x11, 0x68, 0x84, 0x23, 0x88, 0x00,
|
||||
0xBA, 0x03, 0x00, 0x00, 0x00, 0x8B, 0xCF, 0xE8, 0x76, 0x76, 0xEF, 0xFF,
|
||||
};
|
||||
Common::ReplaceWithCall(Offsets::SITE_LUA_STR_GSUB_REPL_CHECK, kGate, sizeof kGate,
|
||||
reinterpret_cast<const void *>(&CheckReplacement));
|
||||
Common::RedirectCall(Offsets::CALL_LUA_STR_GSUB_ADD_S, Offsets::FUN_LUA_STR_ADD_S,
|
||||
reinterpret_cast<const void *>(&AddValue));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Apply};
|
||||
const Game::GlueModuleAutoRegister _glueAutoreg{&Apply};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::StringGsub
|
||||
+38
-124
@@ -15,13 +15,13 @@
|
||||
//
|
||||
// - `string.match(s, pattern [, init])` — first-match extraction (5.1),
|
||||
// also the global `strmatch`, as `string.find` is `strfind`.
|
||||
// - `string.gmatch(s, pattern)` — match iterator (5.1).
|
||||
// - `string.gsub` TABLE replacement — 5.1 form (see the shim below).
|
||||
// - `string.gmatch(s, pattern)` — match iterator (5.1), also `gmatch`.
|
||||
// - `strsplit(sep, str [, pieces])` — WoW global; split on any char.
|
||||
// - `strjoin(delimiter, ...)` — WoW global; join varargs.
|
||||
// - `strtrim(str [, chars])` — WoW global; trim a char set.
|
||||
// - `strreplace(str, find, replace)` — plain-text replace (see below).
|
||||
// - `string.reverse(s)` / `strrev(s)` — 5.1 string function, missing in 5.0.
|
||||
// - `string.byte(s, i, j)` / `strbyte` — 5.1's range form (5.0 reads only i).
|
||||
// - `strlenutf8(s)` — WoW global; characters, not bytes.
|
||||
// - `strcmputf8i(a, b)` — WoW global; case-insensitive UTF-8 order.
|
||||
// - `strsplittable(sep, str [, pieces])`— WoW global; `strsplit` into a table.
|
||||
@@ -341,126 +341,39 @@ int __fastcall Script_string_reverse(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// --- string.gsub table-replacement upgrade (Lua 5.1) ------------------------
|
||||
//
|
||||
// 5.1 allows a TABLE as gsub's replacement: each match looks up the first
|
||||
// capture (the whole match when the pattern declares no captures) in the
|
||||
// table; a nil/false value keeps the original match; string/number values
|
||||
// substitute. This engine's gsub (FUN_LUA_STR_GSUB) is stock 5.0 —
|
||||
// string/function only ("string or function expected" otherwise), and its
|
||||
// add_value appends NOTHING when a replacement function returns a
|
||||
// non-string (match → empty; verified by decode, see Offsets.h). So a
|
||||
// plain table→closure adapter would EAT unmatched tokens where 5.1 keeps
|
||||
// them, and the closure can't reconstruct the whole match from capture
|
||||
// args alone.
|
||||
//
|
||||
// The shim therefore rewrites the pattern with one OUTER capture —
|
||||
// `pat` → `(pat)`, keeping a leading `^` and an unescaped trailing `$`
|
||||
// outside (both are anchors only at their positional ends; inside the
|
||||
// parens they'd turn literal) — so the adapter closure always receives
|
||||
// the whole match as arg 1, the pattern's own first capture as arg 2, and
|
||||
// can hand arg 1 back to reproduce 5.1's keep-match for missing keys.
|
||||
// Patterns containing a `%1`..`%9` back-reference can't be wrapped (the
|
||||
// outer capture would renumber what `%N` refers to); those fall back to
|
||||
// an unwrapped adapter whose missing-key behavior is the engine's own
|
||||
// (match → empty) — the one documented divergence, and the combination
|
||||
// (table replacement + pattern back-references) is essentially absent
|
||||
// from real code. String/function replacements tail-call the engine gsub
|
||||
// untouched.
|
||||
// `string.byte(s [, i [, j]])` / `strbyte` — the codes of bytes i..j, as
|
||||
// 4.3.4's str_byte (j defaults to i). 1.12's (0x7FC210) is 5.0's: it reads
|
||||
// only `i` and pushes one code. With `j` absent the two give the same
|
||||
// result for every `i`. Positions truncate toward zero as the engine's
|
||||
// str_byte and str_sub do. The engine's lua_checkstack (0x6F2F30) refuses
|
||||
// more than 2048 stack slots, so the longest range is about 2000 bytes.
|
||||
|
||||
// True when the pattern contains a `%1`..`%9` back-reference. Escape-aware:
|
||||
// the char after any `%` is consumed, so `%%1` (literal percent, then '1')
|
||||
// doesn't count.
|
||||
bool PatternHasBackref(const char *p, unsigned len) {
|
||||
for (unsigned i = 0; i + 1 < len; ++i) {
|
||||
if (p[i] != '%')
|
||||
continue;
|
||||
const char c = p[i + 1];
|
||||
if (c >= '1' && c <= '9')
|
||||
return true;
|
||||
++i; // skip the escaped char
|
||||
}
|
||||
return false;
|
||||
// 5.1's posrelat: a negative position counts from the end; before the start is 0.
|
||||
long PosRelat(long pos, long len) {
|
||||
if (pos < 0)
|
||||
pos += len + 1;
|
||||
return pos >= 0 ? pos : 0;
|
||||
}
|
||||
|
||||
// Adapter for the wrapped-pattern path: arg 1 = whole match, arg 2 = the
|
||||
// pattern's own first capture (when it declared any). Upvalue 1 = the
|
||||
// replacement table. Missing/false value → return the whole match (5.1's
|
||||
// keep-match).
|
||||
int __fastcall GsubTableLookupWrapped(void *L) {
|
||||
const int nargs = Game::Lua::GetTop(L);
|
||||
Game::Lua::PushValue(L, nargs >= 2 ? 2 : 1); // the 5.1 lookup key
|
||||
Game::Lua::GetTable(L, Game::Lua::UpvalueIndex(1)); // tbl[key]
|
||||
const int t = Game::Lua::Type(L, -1);
|
||||
if (t == Game::Lua::TYPE_NIL ||
|
||||
(t == Game::Lua::TYPE_BOOLEAN && Game::Lua::ToBoolean(L, -1) == 0))
|
||||
Game::Lua::PushValue(L, 1); // keep the original match
|
||||
return 1; // top value is the result
|
||||
}
|
||||
|
||||
// Adapter for back-reference patterns (unwrapped): arg 1 is already the
|
||||
// 5.1 key. A missing key returns nil → the engine appends nothing.
|
||||
int __fastcall GsubTableLookupPlain(void *L) {
|
||||
Game::Lua::PushValue(L, 1);
|
||||
Game::Lua::GetTable(L, Game::Lua::UpvalueIndex(1));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_string_gsub(void *L) {
|
||||
const auto engineGsub =
|
||||
reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_LUA_STR_GSUB);
|
||||
if (Game::Lua::Type(L, 3) != Game::Lua::TYPE_TABLE)
|
||||
return engineGsub(L); // string/function/error paths untouched
|
||||
|
||||
const char *pat = Game::Lua::ToString(L, 2);
|
||||
if (pat == nullptr)
|
||||
return engineGsub(L); // let the engine raise its own arg error
|
||||
const unsigned patLen = Game::Lua::StrLen(L, 2);
|
||||
|
||||
Game::Lua::SetTop(L, 4); // normalize to (s, pat, tbl, n) — gsub reads ≤4
|
||||
|
||||
if (!PatternHasBackref(pat, patLen)) {
|
||||
// Build `(body)` with the anchors outside the capture.
|
||||
unsigned b = 0, e = patLen;
|
||||
std::string wrapped;
|
||||
wrapped.reserve(patLen + 3);
|
||||
if (e > b && pat[0] == '^') {
|
||||
wrapped += '^';
|
||||
b = 1;
|
||||
}
|
||||
bool anchoredBack = false;
|
||||
if (e > b && pat[e - 1] == '$') {
|
||||
// `$` is an anchor only when not %-escaped: an even number of
|
||||
// `%` immediately before it means the `$` itself is unescaped.
|
||||
unsigned pc = 0;
|
||||
for (int idx = static_cast<int>(e) - 2;
|
||||
idx >= static_cast<int>(b) && pat[idx] == '%'; --idx)
|
||||
++pc;
|
||||
anchoredBack = (pc % 2) == 0;
|
||||
if (anchoredBack)
|
||||
--e;
|
||||
}
|
||||
wrapped += '(';
|
||||
wrapped.append(pat + b, e - b);
|
||||
wrapped += ')';
|
||||
if (anchoredBack)
|
||||
wrapped += '$';
|
||||
|
||||
Game::Lua::PushLString(L, wrapped.data(),
|
||||
static_cast<unsigned int>(wrapped.size())); // 5
|
||||
Game::Lua::PushValue(L, 3); // 6: tbl
|
||||
Game::Lua::PushCClosure(L, &GsubTableLookupWrapped, 1); // 6: closure
|
||||
Game::Lua::Insert(L, 2); // closure→2: s, closure, pat, tbl, n, wrapped
|
||||
Game::Lua::Insert(L, 2); // wrapped→2: s, wrapped, closure, pat, tbl, n
|
||||
Game::Lua::Remove(L, 4); // drop the original pattern
|
||||
Game::Lua::Remove(L, 4); // drop the table → (s, wrapped, closure, n)
|
||||
} else {
|
||||
Game::Lua::PushValue(L, 3); // 5: tbl
|
||||
Game::Lua::PushCClosure(L, &GsubTableLookupPlain, 1); // 5: closure
|
||||
Game::Lua::Insert(L, 3); // → s, pat, closure, tbl, n
|
||||
Game::Lua::Remove(L, 4); // → s, pat, closure, n
|
||||
}
|
||||
return engineGsub(L);
|
||||
int __fastcall Script_string_byte(void *L) {
|
||||
unsigned int l = 0;
|
||||
const char *s = Game::Lua::CheckLString(L, 1, &l);
|
||||
const long len = static_cast<long>(l);
|
||||
long posi = PosRelat(static_cast<long>(Game::Lua::OptNumber(L, 2, 1)), len);
|
||||
long pose =
|
||||
PosRelat(static_cast<long>(Game::Lua::OptNumber(L, 3, static_cast<double>(posi))), len);
|
||||
if (posi <= 0)
|
||||
posi = 1;
|
||||
if (pose > len)
|
||||
pose = len;
|
||||
if (posi > pose)
|
||||
return 0;
|
||||
const int n = static_cast<int>(pose - posi + 1);
|
||||
if (!Game::Lua::CheckStack(L, n))
|
||||
Game::Lua::Error(L, "stack overflow (%s)", "string slice too long");
|
||||
for (int i = 0; i < n; i++)
|
||||
Game::Lua::PushNumber(L, static_cast<unsigned char>(s[posi + i - 1]));
|
||||
return n;
|
||||
}
|
||||
|
||||
// --- Documentation ----------------------------------------------------------
|
||||
@@ -515,27 +428,28 @@ const Game::Doc::Function kStrconcat{
|
||||
void RegisterFns() {
|
||||
Game::Lua::RegisterTableFunction("string", "match", &Script_string_match);
|
||||
Game::Lua::RegisterTableFunction("string", "gmatch", Script_string_gmatch);
|
||||
Game::Lua::RegisterTableFunction("string", "gsub", &Script_string_gsub);
|
||||
Game::Lua::RegisterTableFunction("string", "reverse", &Script_string_reverse);
|
||||
Game::Lua::RegisterTableFunction("string", "byte", &Script_string_byte);
|
||||
Game::Lua::RegisterTableFunction("string", "split", &Script_strsplit);
|
||||
Game::Lua::RegisterTableFunction("string", "trim", &Script_strtrim);
|
||||
Game::Lua::RegisterTableFunction("string", "join", &Script_strjoin);
|
||||
Game::Lua::RegisterTableFunction("string", "concat", &Script_strconcat, &kStrconcat);
|
||||
// 4.3.4's compat.lua binds each short global and its string-table twin
|
||||
// to ONE function by assignment (`gsub = str.gsub`, `strmatch =
|
||||
// to ONE function by assignment (`gmatch = str.gmatch`, `strmatch =
|
||||
// str.match`, `strrev = str.reverse`, `str.trim = strtrim`, `str.split =
|
||||
// strsplit`, `str.join = strjoin`), so `strtrim == string.trim`. A second
|
||||
// Register* call would mint a second closure over the same C function, so
|
||||
// the globals alias the table entries by value instead. The string-table
|
||||
// half is what lets `(" x "):trim()` work and libraries capture it at
|
||||
// file scope (AceConfigCmd-3.0: `strtrim = string.trim`); the bare `gsub`
|
||||
// gets the table-replacement upgrade by being the same function.
|
||||
Game::Lua::RegisterGlobalAlias("gsub", "string", "gsub");
|
||||
// file scope (AceConfigCmd-3.0: `strtrim = string.trim`). 1.12's own
|
||||
// compat.lua set `strbyte` from the old string.byte, so it is re-aliased.
|
||||
Game::Lua::RegisterGlobalAlias("gmatch", "string", "gmatch");
|
||||
Game::Lua::RegisterGlobalAlias("strmatch", "string", "match");
|
||||
Game::Lua::RegisterGlobalAlias("strsplit", "string", "split");
|
||||
Game::Lua::RegisterGlobalAlias("strjoin", "string", "join");
|
||||
Game::Lua::RegisterGlobalAlias("strtrim", "string", "trim");
|
||||
Game::Lua::RegisterGlobalAlias("strrev", "string", "reverse");
|
||||
Game::Lua::RegisterGlobalAlias("strbyte", "string", "byte");
|
||||
Game::Lua::RegisterGlobalAlias("strconcat", "string", "concat", &kStrconcat);
|
||||
Game::Lua::RegisterGlobalFunction("strreplace", &Script_strreplace);
|
||||
Game::Lua::RegisterGlobalFunction("strlenutf8", &Script_strlenutf8, &kStrlenutf8);
|
||||
|
||||
+6
-22
@@ -47,7 +47,9 @@
|
||||
// section's header follows.
|
||||
|
||||
#include "Inject.h"
|
||||
#include "Common.h"
|
||||
#include "Offsets.h"
|
||||
#include "storm/Memory.h"
|
||||
|
||||
#include "embed/Resource.h"
|
||||
#include "embedded_resources.h"
|
||||
@@ -133,7 +135,7 @@ bool ExtractHeaderAttr(const char *tagStart, const char *tagEnd,
|
||||
|
||||
EmbeddedConfig ResolveEmbeddedConfig() {
|
||||
EmbeddedConfig out = {};
|
||||
const Embed::Bytes xml = Embed::Resource(Embed::Ids::kBindingsXml);
|
||||
const Embed::Bytes xml = Embed::File(Embed::Ids::kBindingsXml, 0);
|
||||
if (xml.data == nullptr) return out;
|
||||
const char *content = reinterpret_cast<const char *>(xml.data);
|
||||
const char *end = content + xml.size;
|
||||
@@ -212,28 +214,12 @@ const char *FindSplicePosition(const char *content, size_t size,
|
||||
return FindOutsideComment(content, end, "</Bindings>");
|
||||
}
|
||||
|
||||
char NormalizeChar(char c) {
|
||||
if (c == '/') return '\\';
|
||||
if (c >= 'A' && c <= 'Z') return static_cast<char>(c + 32);
|
||||
return c;
|
||||
}
|
||||
|
||||
bool PathEqualsCI(const char *a, const char *b) {
|
||||
while (*a != '\0' && *b != '\0') {
|
||||
if (NormalizeChar(*a) != NormalizeChar(*b)) return false;
|
||||
++a; ++b;
|
||||
}
|
||||
return *a == *b;
|
||||
}
|
||||
|
||||
using SMemAlloc_t = void *(__stdcall *)(size_t size, const char *file, int line, int flags);
|
||||
using SMemFree_t = void(__stdcall *)(void *buf, const char *file, int line, int flags);
|
||||
|
||||
} // namespace
|
||||
|
||||
bool TryHandle(int unused, const char *path, void **outBuf, size_t *outSize,
|
||||
size_t extraBytes, int flag1, int flag2, FileReadFn original) {
|
||||
if (path == nullptr || !PathEqualsCI(path, kFrameXMLBindingsPath))
|
||||
if (path == nullptr || !Common::SamePath(path, kFrameXMLBindingsPath))
|
||||
return false;
|
||||
|
||||
// Pull the real file through the original reader with the caller's
|
||||
@@ -270,10 +256,8 @@ bool TryHandle(int unused, const char *path, void **outBuf, size_t *outSize,
|
||||
const size_t splicePos = static_cast<size_t>(splice - content);
|
||||
const size_t newSize = origSize + config.innerSize;
|
||||
|
||||
auto SMemAlloc = reinterpret_cast<SMemAlloc_t>(Offsets::FUN_STORM_SMEM_ALLOC);
|
||||
auto SMemFree = reinterpret_cast<SMemFree_t>(Offsets::FUN_STORM_SMEM_FREE);
|
||||
|
||||
void *newBuf = SMemAlloc(newSize + extraBytes, __FILE__, __LINE__, 0);
|
||||
void *newBuf = Storm::Alloc(newSize + extraBytes, __FILE__, __LINE__);
|
||||
if (newBuf == nullptr) {
|
||||
// Allocation failed — fall back to the original buffer so the
|
||||
// engine still loads its own bindings.
|
||||
@@ -290,7 +274,7 @@ bool TryHandle(int unused, const char *path, void **outBuf, size_t *outSize,
|
||||
if (extraBytes > 0)
|
||||
std::memset(dest + newSize, 0, extraBytes);
|
||||
|
||||
SMemFree(origBuf, __FILE__, __LINE__, 0);
|
||||
Storm::Free(origBuf, __FILE__, __LINE__);
|
||||
|
||||
if (outBuf != nullptr) *outBuf = newBuf;
|
||||
if (outSize != nullptr) *outSize = newSize;
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
#include "baselib/Ascii.h"
|
||||
#include "frame/ClickEvents.h"
|
||||
#include "input/Modifier.h"
|
||||
#include "storm/Memory.h"
|
||||
#include "xml/FrameLoad.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -219,8 +220,7 @@ void LoadModifiedClicks(const char *path, void *status) {
|
||||
return;
|
||||
void *tree = reinterpret_cast<TreeLoad_t>(Offsets::FUN_XML_TREE_LOAD)(
|
||||
static_cast<const char *>(data), static_cast<uint32_t>(size));
|
||||
reinterpret_cast<AddOns::EngineIO::SMemFreeFn>(Offsets::FUN_STORM_SMEM_FREE)(data, __FILE__,
|
||||
__LINE__, 0);
|
||||
Storm::Free(data, __FILE__, __LINE__);
|
||||
if (tree == nullptr)
|
||||
return; // the engine loader reports the parse error
|
||||
const void *root = Game::Read<const void *>(tree, 0);
|
||||
|
||||
@@ -61,10 +61,7 @@ const char *UnitName(uint64_t guid) {
|
||||
if (guid == 0)
|
||||
return nullptr;
|
||||
if (void *unit = Object::ByGuid(Offsets::TYPEMASK_UNIT, guid, nullptr, 0)) {
|
||||
using GetName_t = const char *(__thiscall *)(void *obj, int *outFlags);
|
||||
const char *name =
|
||||
reinterpret_cast<GetName_t>(static_cast<uintptr_t>(Offsets::FUN_OBJECT_GET_NAME))(
|
||||
unit, nullptr);
|
||||
const char *name = Unit::Identity::NameForUnit(unit);
|
||||
if (name != nullptr && *name != '\0')
|
||||
return name;
|
||||
}
|
||||
|
||||
+10
-2
@@ -29,6 +29,8 @@
|
||||
// marker icons but still can't inject an arbitrary `|T` path.
|
||||
// - Chat::EventArgs::SenderScope — name the sender for the CHAT_MSG_* event
|
||||
// the original fires synchronously: its arg12 and `GetCurrentChatGUID()`.
|
||||
// - AutoComplete::Names::OnChat — a whisper partner becomes an
|
||||
// InteractedWith name for C_AutoComplete.
|
||||
//
|
||||
// Signature — `__fastcall`, `RET 0x28` (stack args at entry):
|
||||
// ECX = the MESSAGE text (`MOV ECX,[EBP-0x10]` in FUN_0049D560; copied into
|
||||
@@ -68,6 +70,7 @@
|
||||
#include "RaidMarkers.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "autocomplete/Names.h"
|
||||
#include "Offsets.h"
|
||||
#include "net/PacketDispatch.h"
|
||||
#include "net/PacketReader.h"
|
||||
@@ -158,10 +161,15 @@ void __fastcall ChatDispatch_h(
|
||||
// escapes are added.
|
||||
Chat::RaidMarkers::EventScope markers;
|
||||
|
||||
const uint64_t senderGuid = Sender(_ReturnAddress(), chatType, channel, flags, guidLo, guidHi);
|
||||
|
||||
// A whisper partner becomes an autocomplete name before the event fires,
|
||||
// so a CHAT_MSG_WHISPER handler already finds them.
|
||||
AutoComplete::Names::OnChat(chatType, languageID, senderName, senderGuid);
|
||||
|
||||
// The sender for the CHAT_MSG_* event the original fires synchronously;
|
||||
// restored when this scope ends (after the original returns).
|
||||
Chat::EventArgs::SenderScope sender(
|
||||
Sender(_ReturnAddress(), chatType, channel, flags, guidLo, guidHi), flags, true);
|
||||
Chat::EventArgs::SenderScope sender(senderGuid, flags, true);
|
||||
|
||||
g_original(msg, chatType, senderName, languageID, channel, targetName, flags,
|
||||
speakerGuidLo, speakerGuidHi, rank, guidLo, guidHi);
|
||||
|
||||
+38
-62
@@ -25,7 +25,8 @@
|
||||
// - a cast-time spell is cast -> SMSG_SPELL_GO, InterruptFlags & AUTOATTACK
|
||||
// - autorepeat (Auto Shot / Shoot) fires -> SMSG_SPELL_GO, AttributesEx2 & AUTOREPEAT
|
||||
// - starting a fresh melee attack -> SMSG_ATTACKSTART (off-hand only)
|
||||
// - a weapon is swapped mid-combat -> no packet; polled off the invMgr GUIDs
|
||||
// - a weapon is swapped mid-combat -> no packet; the engine's inventory-slot
|
||||
// observer (via Player::Equipment)
|
||||
// - the LOCAL PLAYER parries an attack -> SMSG_ATTACKERSTATEUPDATE, victim == us,
|
||||
// targetState == PARRY (no separate packet —
|
||||
// the server just clips OUR remaining time)
|
||||
@@ -71,18 +72,25 @@ struct State {
|
||||
uint32_t endMs = 0;
|
||||
bool active = false; // at least one reset has happened this session
|
||||
bool dirty = false; // needs a PLAYER_SWING fire on the next WorldTick
|
||||
bool held = false; // the latest reset was recorded but not reported yet
|
||||
};
|
||||
State g_swing[3];
|
||||
|
||||
// `notify` false records the server's new timer without firing PLAYER_SWING:
|
||||
// the reset is real, but no swing is coming yet (see OnSpellGo).
|
||||
// the reset is real, but no swing is coming yet (see OnSpellGo). It stays
|
||||
// `held` until the attack start reports it or a later reported reset replaces it.
|
||||
void MarkReset(SwingType type, uint32_t durationMs, bool notify = true) {
|
||||
if (durationMs == 0)
|
||||
return; // no weapon for this hand right now -- nothing to report
|
||||
State &s = g_swing[type];
|
||||
s.endMs = NowMs() + durationMs;
|
||||
s.active = true;
|
||||
s.dirty = s.dirty || notify;
|
||||
if (notify) {
|
||||
s.dirty = true;
|
||||
s.held = false;
|
||||
} else {
|
||||
s.held = true;
|
||||
}
|
||||
}
|
||||
|
||||
using HasAttackTarget_t = int(__fastcall *)(const void *unit);
|
||||
@@ -409,11 +417,17 @@ void OnPacket(uint32_t opcode, CDataStore *packet) {
|
||||
auto *playerObj = const_cast<uint8_t *>(Unit::Identity::PlayerObject());
|
||||
if (InMeleeRange(playerObj, victimObj)) {
|
||||
MarkReset(OFF_HAND, AttackTimeMs(OFF_HAND));
|
||||
// A cast reset made before the attack still holds the
|
||||
// main-hand swing back; report it now that one is coming.
|
||||
// A cast reset made before the attack is still held back;
|
||||
// report it now that a swing is coming. Only a held reset:
|
||||
// a target switch mid-attack also lands here, and there the
|
||||
// main-hand swing was never reset -- re-reporting its
|
||||
// remaining time would read as a fresh, short swing.
|
||||
State &main = g_swing[MAIN_HAND];
|
||||
if (main.active && Time::Clock::Remaining(NowMs(), main.endMs) > 0)
|
||||
main.dirty = true;
|
||||
if (main.held) {
|
||||
main.held = false;
|
||||
if (Time::Clock::Remaining(NowMs(), main.endMs) > 0)
|
||||
main.dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -431,64 +445,29 @@ void OnPacket(uint32_t opcode, CDataStore *packet) {
|
||||
|
||||
const Net::PacketDispatch::AutoSubscribe _sub{&OnPacket};
|
||||
|
||||
// ---- Weapon swap mid-combat (polled -- no packet, no observer needed) ---
|
||||
} // namespace
|
||||
|
||||
// ---- Weapon swap mid-combat (inventory-slot observer) -------------------
|
||||
//
|
||||
// A weapon equipped while `IsInCombat()` resets that hand's timer
|
||||
// server-side (`Player::_ApplyWeaponDependentAuraMods` -> `SetAttackTime`
|
||||
// with `resetTimer = IsInCombat()`). There's no dedicated packet for this;
|
||||
// `Player::Equipment` already sees the same invMgr GUIDs change via the
|
||||
// engine's inventory-slot observer for `PLAYER_EQUIPMENT_CHANGED`, but
|
||||
// that's a one-way Lua event fire with nothing to subscribe to in C++. Only
|
||||
// 3 slots matter here, so a plain 3-GUID compare on the existing WorldTick
|
||||
// pass is simpler than a second hook on the same observer.
|
||||
|
||||
uint64_t g_lastMainGuid = 0;
|
||||
uint64_t g_lastOffGuid = 0;
|
||||
uint64_t g_lastRangedGuid = 0;
|
||||
bool g_weaponSnapValid = false;
|
||||
|
||||
void PollWeaponSwap() {
|
||||
const uint8_t *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr) {
|
||||
g_weaponSnapValid = false;
|
||||
return;
|
||||
}
|
||||
const uint8_t *invMgr = player + Offsets::OFF_PLAYER_INVENTORY_MANAGER;
|
||||
const uint32_t count =
|
||||
*reinterpret_cast<const uint32_t *>(invMgr + Offsets::OFF_INVMGR_SLOT_COUNT);
|
||||
if (count < 18) {
|
||||
g_weaponSnapValid = false;
|
||||
return;
|
||||
}
|
||||
const uint64_t *guids = *reinterpret_cast<const uint64_t *const *>(
|
||||
invMgr + Offsets::OFF_INVMGR_GUID_ARRAY);
|
||||
if (guids == nullptr) {
|
||||
g_weaponSnapValid = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// `Player::Equipment`'s co-hook on the engine's inventory-slot observer calls
|
||||
// in here when a slot's GUID changes. That runs inside SMSG_UPDATE_OBJECT
|
||||
// after the whole values block is written, so `AttackTimeMs` already reads
|
||||
// the new weapon's speed when it arrives in the same block.
|
||||
void OnWeaponSlotChanged(int slot0) {
|
||||
// 0-based invMgr slots: 15 = main hand (Lua 16), 16 = off hand (17),
|
||||
// 17 = ranged (18) -- same `slot0 = luaSlot - 1` convention documented
|
||||
// in Player::Equipment.
|
||||
const uint64_t mainGuid = guids[15];
|
||||
const uint64_t offGuid = guids[16];
|
||||
const uint64_t rangedGuid = guids[17];
|
||||
|
||||
if (g_weaponSnapValid && PlayerInCombat()) {
|
||||
if (mainGuid != g_lastMainGuid)
|
||||
MarkReset(MAIN_HAND, AttackTimeMs(MAIN_HAND));
|
||||
if (offGuid != g_lastOffGuid)
|
||||
MarkReset(OFF_HAND, AttackTimeMs(OFF_HAND));
|
||||
if (rangedGuid != g_lastRangedGuid)
|
||||
MarkReset(RANGED, AttackTimeMs(RANGED));
|
||||
}
|
||||
|
||||
g_lastMainGuid = mainGuid;
|
||||
g_lastOffGuid = offGuid;
|
||||
g_lastRangedGuid = rangedGuid;
|
||||
g_weaponSnapValid = true;
|
||||
// in Player::Equipment, and in SwingType order.
|
||||
if (slot0 < 15 || slot0 > 17 || !PlayerInCombat())
|
||||
return;
|
||||
const auto type = static_cast<SwingType>(slot0 - 15);
|
||||
MarkReset(type, AttackTimeMs(type));
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// ---- PLAYER_SWING fire (WorldTick, at most once per type per frame) -----
|
||||
|
||||
const Game::Doc::Field kPayload[] = {
|
||||
@@ -504,8 +483,6 @@ const Game::Doc::Event kPlayerSwingDoc{
|
||||
const Event::Custom::AutoReserve _evt{"PLAYER_SWING", &kPlayerSwingDoc};
|
||||
|
||||
void OnWorldTick() {
|
||||
PollWeaponSwap();
|
||||
|
||||
const int slot = _evt.Slot();
|
||||
const uint32_t now = NowMs();
|
||||
for (int t = 0; t < 3; ++t) {
|
||||
@@ -513,6 +490,7 @@ void OnWorldTick() {
|
||||
if (!s.dirty)
|
||||
continue;
|
||||
s.dirty = false;
|
||||
s.held = false;
|
||||
if (slot < 0)
|
||||
continue;
|
||||
const uint32_t remaining = Time::Clock::Remaining(now, s.endMs);
|
||||
@@ -533,9 +511,7 @@ const Game::Lua::EnumIntegerEntry kSwingTypeEntries[] = {
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterIntegerEnum(
|
||||
"Enum", "PlayerSwingType", kSwingTypeEntries,
|
||||
sizeof(kSwingTypeEntries) / sizeof(kSwingTypeEntries[0]), "SwingTimer");
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "PlayerSwingType", kSwingTypeEntries, "SwingTimer");
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -32,6 +32,12 @@ enum SwingType : int { MAIN_HAND = 0, OFF_HAND = 1, RANGED = 2 };
|
||||
// callers treat 0 as "this swing type doesn't apply right now".
|
||||
uint32_t AttackTimeMs(SwingType type);
|
||||
|
||||
// Called by `Player::Equipment` from the engine's inventory-slot observer
|
||||
// when the item GUID in 0-based equipment slot `slot0` changes. A weapon
|
||||
// slot (15 / 16 / 17) changed in combat resets that hand's swing timer; any
|
||||
// other slot is ignored.
|
||||
void OnWeaponSlotChanged(int slot0);
|
||||
|
||||
// True if `player` and `target` (raw CGUnit* object pointers, e.g. from
|
||||
// `Unit::Position::ResolveToken` or `Object::ByGuid`) are within pure 2D
|
||||
// (X/Y) melee reach of each other -- the server's own auto-attack distance
|
||||
|
||||
+21
-24
@@ -35,6 +35,7 @@
|
||||
#include "combatlog/Entry.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "combatlog/File.h"
|
||||
#include "combatlog/History.h"
|
||||
#include "Offsets.h"
|
||||
#include "event/Custom.h"
|
||||
@@ -44,6 +45,7 @@
|
||||
#include "time/Clock.h"
|
||||
#include "unit/Focus.h"
|
||||
#include "unit/Identity.h"
|
||||
#include "unit/Power.h"
|
||||
|
||||
#include <ctime>
|
||||
|
||||
@@ -298,7 +300,6 @@ using CacheRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t id, co
|
||||
using NameCache_t = const uint8_t *(__thiscall *)(void *cache, uint32_t lo, uint32_t hi,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, char dedup);
|
||||
using ObjectName_t = const char *(__thiscall *)(void *object, int *outFlags);
|
||||
using GameObjectName_t = const char *(__fastcall *)(void *object);
|
||||
|
||||
void *QueryDone() { return reinterpret_cast<void *>(Offsets::FUN_COMBAT_LOG_NAME_QUERY_DONE); }
|
||||
@@ -322,7 +323,7 @@ const char *ObjectName(uint64_t guid) {
|
||||
const uint32_t type =
|
||||
fields != nullptr ? Game::Read<uint32_t>(fields, Offsets::OFF_OBJECT_FIELD_TYPE) : 0;
|
||||
if (type & Offsets::TYPEMASK_UNIT)
|
||||
return reinterpret_cast<ObjectName_t>(Offsets::FUN_OBJECT_GET_NAME)(object, nullptr);
|
||||
return Unit::Identity::NameForUnit(object);
|
||||
if (type & Offsets::TYPEMASK_GAMEOBJECT)
|
||||
return reinterpret_cast<GameObjectName_t>(Offsets::FUN_GAMEOBJECT_GET_NAME)(object);
|
||||
if (type & Offsets::TYPEMASK_ITEM)
|
||||
@@ -406,9 +407,7 @@ int Push(void *L, const Entry &e) {
|
||||
PushString(L, name);
|
||||
else
|
||||
PushNil(L);
|
||||
PushNumber(L, spell != nullptr
|
||||
? SchoolMask(Game::Read<uint32_t>(spell, Offsets::OFF_SPELL_RECORD_SCHOOL))
|
||||
: e.spellSchool);
|
||||
PushNumber(L, spell != nullptr ? SpellSchoolMask(spell) : e.spellSchool);
|
||||
count += 3;
|
||||
}
|
||||
if (e.info & INFO_COUNT) {
|
||||
@@ -420,14 +419,12 @@ int Push(void *L, const Entry &e) {
|
||||
++count;
|
||||
}
|
||||
if (e.info & INFO_MISS) {
|
||||
const int miss = e.v[0];
|
||||
PushString(L, (miss >= 0 && miss < MISS_COUNT) ? kMissNames[miss] : kMissNames[0]);
|
||||
PushString(L, MissName(e.v[0]));
|
||||
PushFlag(L, e.offHand);
|
||||
count += 2;
|
||||
}
|
||||
if (e.info & INFO_ENVIRONMENTAL) {
|
||||
const int type = e.v[0];
|
||||
PushString(L, (type >= 0 && type < 6) ? kEnvironmentalNames[type] : kEnvironmentalNames[0]);
|
||||
PushString(L, EnvironmentalName(e.v[0]));
|
||||
++count;
|
||||
}
|
||||
if (e.info & INFO_DAMAGE) {
|
||||
@@ -489,9 +486,7 @@ int Push(void *L, const Entry &e) {
|
||||
PushString(L, name);
|
||||
else
|
||||
PushNil(L);
|
||||
PushNumber(L, extra != nullptr ? SchoolMask(Game::Read<uint32_t>(
|
||||
extra, Offsets::OFF_SPELL_RECORD_SCHOOL))
|
||||
: 0);
|
||||
PushNumber(L, SpellSchoolMask(extra));
|
||||
count += 3;
|
||||
}
|
||||
if (e.info & INFO_ITEM) {
|
||||
@@ -582,6 +577,14 @@ const char *Name(int event) {
|
||||
return (event >= 0 && event < EVENT_COUNT) ? kEventNames[event] : "";
|
||||
}
|
||||
|
||||
const char *MissName(int missType) {
|
||||
return (missType >= 0 && missType < MISS_COUNT) ? kMissNames[missType] : kMissNames[0];
|
||||
}
|
||||
|
||||
const char *EnvironmentalName(int type) {
|
||||
return (type >= 0 && type < 6) ? kEnvironmentalNames[type] : kEnvironmentalNames[0];
|
||||
}
|
||||
|
||||
int PushEvent(void *L, const Entry &entry) { return Push(L, entry); }
|
||||
|
||||
const uint8_t *SpellRecord(int spellId) { return Spell::Lookup::RecordForID(spellId); }
|
||||
@@ -609,15 +612,10 @@ bool Ranged(const uint8_t *spell) {
|
||||
|
||||
uint32_t SchoolMask(uint32_t school) { return school < kSchoolCount ? 1u << school : 0; }
|
||||
|
||||
bool HasAuraEffect(const uint8_t *spell, int auraName) {
|
||||
if (spell == nullptr)
|
||||
return false;
|
||||
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i) {
|
||||
if (Game::Read<int>(spell, Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME + i * 4) ==
|
||||
auraName)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
uint32_t SpellSchoolMask(const uint8_t *spell) {
|
||||
return spell != nullptr
|
||||
? SchoolMask(Game::Read<uint32_t>(spell, Offsets::OFF_SPELL_RECORD_SCHOOL))
|
||||
: 0;
|
||||
}
|
||||
|
||||
Entry Begin(uint64_t source, uint64_t dest, int event, const uint8_t *spell) {
|
||||
@@ -676,9 +674,7 @@ void SetItem(Entry &e, int itemId) {
|
||||
}
|
||||
|
||||
int DisplayAmount(int amount, int powerType) {
|
||||
using Divisor_t = int(__fastcall *)(int powerType);
|
||||
const int divisor = reinterpret_cast<Divisor_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_POWER_DISPLAY_DIVISOR))(powerType);
|
||||
const int divisor = static_cast<int>(Unit::Power::DisplayDivisor(powerType));
|
||||
return divisor != 0 ? amount / divisor : amount;
|
||||
}
|
||||
|
||||
@@ -701,6 +697,7 @@ void Queue(Entry &e) {
|
||||
g_startTimeMs = e.timeMs;
|
||||
}
|
||||
History::Record(e);
|
||||
File::Write(e);
|
||||
Send(e);
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,12 @@ enum Subevent : int {
|
||||
|
||||
const char *Name(int event);
|
||||
|
||||
// The INFO_MISS value as 4.3.4's g_spellMissStrings names it.
|
||||
const char *MissName(int missType);
|
||||
|
||||
// The INFO_ENVIRONMENTAL value as 4.3.4's s_environmentalDamageTypes names it.
|
||||
const char *EnvironmentalName(int type);
|
||||
|
||||
// 4.3.4's m_infoFlags: which suffix blocks the event carries, pushed in
|
||||
// this order after the spell prefix.
|
||||
enum Info : uint32_t {
|
||||
@@ -158,9 +164,8 @@ bool Ranged(const uint8_t *spell);
|
||||
// A school index (Spell.dbc School, the packets' school) as a school mask.
|
||||
uint32_t SchoolMask(uint32_t school);
|
||||
|
||||
// Whether one of the spell's three effects applies the aura `auraName`
|
||||
// (EffectApplyAuraName).
|
||||
bool HasAuraEffect(const uint8_t *spell, int auraName);
|
||||
// The spell's school as a mask (4.3.4's SpellRec m_schoolMask), 0 for nullptr.
|
||||
uint32_t SpellSchoolMask(const uint8_t *spell);
|
||||
|
||||
// GetCombatLogEntry: an entry for `event` from `source` to `dest` (either may
|
||||
// be 0), with the spell prefix when `spell` is a record.
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// The combat log file — 4.3.4's CombatLogEntry::PackEvent line, written as
|
||||
// SendCombatLogEvents writes it while LoggingCombat is on.
|
||||
//
|
||||
// The engine pieces:
|
||||
// - the switch is LoggingCombat's own flag, so /combatlog and
|
||||
// LoggingCombat() turn both files on and off, and logging out (the chat
|
||||
// teardown in CGGameUI::ShutdownGame, 0x00498700, clears the flag) stops
|
||||
// both;
|
||||
// - each time the flag turns on, a new file starts, named as retail names
|
||||
// it: Logs\WoWCombatLog-MMDDYY_HHMMSS.txt, the local time logging started.
|
||||
// 1.12's own Logs\WoWCombatLog.txt keeps its chat-text lines, which 1.12
|
||||
// log readers parse;
|
||||
// - the file is a Storm log, opened as LoggingCombat opens its own (append)
|
||||
// and written with SLogWrite (which stamps "M/D HH:MM:SS.mmm " on each
|
||||
// line, as in 4.3.4's file). When the flag turns off it is closed, which
|
||||
// writes its buffered lines. Storm closes what is still open at exit;
|
||||
// - the line is PackEvent's: no hideCaster, flags as hex, the spell school
|
||||
// as a hex mask (the extra spell's in decimal), damage values as numbers,
|
||||
// flags as 1 or nil, no isOffHand and no alternate power type.
|
||||
//
|
||||
// The flag is read once a frame (FrameTick, which also runs on the glue
|
||||
// screen, so a logout's cleared flag is seen there) and before each line.
|
||||
// LoggingCombat itself is not hooked: other DLLs add their own logs to it.
|
||||
|
||||
#include "combatlog/File.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "guid/Guid.h"
|
||||
#include "tick/FrameTick.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
#include <string>
|
||||
|
||||
namespace CombatLog::File {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kAppend = 4; // SLogCreate: open at the end of the file
|
||||
|
||||
uint32_t g_log = 0;
|
||||
|
||||
bool Logging() { return Game::Read<int>(Offsets::VAR_COMBAT_LOGGING) != 0; }
|
||||
|
||||
// A new log named for the local time now. True when it is open.
|
||||
bool Open() {
|
||||
const std::time_t now = std::time(nullptr);
|
||||
std::tm local{};
|
||||
if (localtime_s(&local, &now) != 0)
|
||||
return false;
|
||||
char path[64];
|
||||
std::strftime(path, sizeof path, "Logs\\WoWCombatLog-%m%d%y_%H%M%S.txt", &local);
|
||||
using Create_t = int(__stdcall *)(const char *path, uint32_t flags, uint32_t *log);
|
||||
const auto create = reinterpret_cast<Create_t>(static_cast<uintptr_t>(Offsets::FUN_SLOG_CREATE));
|
||||
if (create(path, kAppend, &g_log) == 0)
|
||||
g_log = 0;
|
||||
return g_log != 0;
|
||||
}
|
||||
|
||||
void Close() {
|
||||
using Close_t = void(__stdcall *)(uint32_t log);
|
||||
reinterpret_cast<Close_t>(static_cast<uintptr_t>(Offsets::FUN_SLOG_CLOSE))(g_log);
|
||||
g_log = 0;
|
||||
}
|
||||
|
||||
// Follows the flag: a new file when it turns on, the file closed when it
|
||||
// turns off.
|
||||
void OnFrame() {
|
||||
const bool logging = Logging();
|
||||
if (logging && g_log == 0)
|
||||
Open();
|
||||
else if (!logging && g_log != 0)
|
||||
Close();
|
||||
}
|
||||
|
||||
const Tick::FrameTick::AutoSubscribe _frame{&OnFrame};
|
||||
|
||||
// PackString
|
||||
void Plain(std::string &line, const char *text) {
|
||||
line += ',';
|
||||
line += text;
|
||||
}
|
||||
|
||||
// PackQuotedString, or PackString "nil" when there is no text.
|
||||
void Quoted(std::string &line, const char *text) {
|
||||
if (text == nullptr) {
|
||||
Plain(line, "nil");
|
||||
return;
|
||||
}
|
||||
line += ",\"";
|
||||
for (const char *c = text; *c != '\0'; ++c) {
|
||||
if (*c == '"' || *c == '\\')
|
||||
line += '\\';
|
||||
line += *c;
|
||||
}
|
||||
line += '"';
|
||||
}
|
||||
|
||||
// PackHex
|
||||
void Hex(std::string &line, uint32_t value) {
|
||||
char text[16];
|
||||
std::snprintf(text, sizeof text, ",0x%x", value);
|
||||
line += text;
|
||||
}
|
||||
|
||||
void Number(std::string &line, int value) {
|
||||
char text[16];
|
||||
std::snprintf(text, sizeof text, ",%d", value);
|
||||
line += text;
|
||||
}
|
||||
|
||||
// PackBool
|
||||
void Flag(std::string &line, bool value) { line += value ? ",1" : ",nil"; }
|
||||
|
||||
void Unit(std::string &line, uint64_t guid, bool named, const std::string &name, uint32_t flags,
|
||||
uint32_t raidFlags) {
|
||||
char text[Guid::STRING_SIZE];
|
||||
line += ',';
|
||||
line += Guid::FormatAsString(guid, text, sizeof text);
|
||||
Quoted(line, named ? name.c_str() : nullptr);
|
||||
Hex(line, flags);
|
||||
Hex(line, raidFlags);
|
||||
}
|
||||
|
||||
// CombatLogEntry::PackEvent, the blocks 1.12 fills.
|
||||
std::string Pack(const Entry &e) {
|
||||
std::string line = Name(e.event);
|
||||
Unit(line, e.source, e.sourceNamed, e.sourceName, e.sourceFlags, e.sourceRaidFlags);
|
||||
Unit(line, e.dest, e.destNamed, e.destName, e.destFlags, e.destRaidFlags);
|
||||
if (e.spellPrefix) {
|
||||
const uint8_t *spell = SpellRecord(e.spellId);
|
||||
Number(line, e.spellId);
|
||||
Quoted(line, SpellName(spell));
|
||||
Hex(line, spell != nullptr ? SpellSchoolMask(spell) : e.spellSchool);
|
||||
}
|
||||
if (e.info & INFO_COUNT)
|
||||
Number(line, e.v[0]);
|
||||
if (e.info & INFO_TEXT)
|
||||
Quoted(line, e.text.c_str());
|
||||
if (e.info & INFO_MISS)
|
||||
Plain(line, MissName(e.v[0]));
|
||||
if (e.info & INFO_ENVIRONMENTAL)
|
||||
Plain(line, EnvironmentalName(e.v[0]));
|
||||
if (e.info & INFO_DAMAGE) {
|
||||
for (const int slot : {1, 2, 3, 5, 6, 4})
|
||||
Number(line, e.v[slot]);
|
||||
Flag(line, e.critical);
|
||||
Flag(line, e.glancing);
|
||||
Flag(line, e.crushing);
|
||||
}
|
||||
if (e.info & INFO_MISS_ABSORBED)
|
||||
Number(line, e.v[4]);
|
||||
if (e.info & INFO_MISS_RESISTED)
|
||||
Number(line, e.v[5]);
|
||||
if (e.info & INFO_MISS_BLOCKED)
|
||||
Number(line, e.v[6]);
|
||||
if (e.info & INFO_HEAL) {
|
||||
for (int i = 0; i < 3; ++i)
|
||||
Number(line, e.v[i]);
|
||||
Flag(line, e.critical);
|
||||
}
|
||||
if (e.info & INFO_ENERGIZE) {
|
||||
for (int i = 0; i < 2; ++i)
|
||||
Number(line, e.v[i]);
|
||||
}
|
||||
if (e.info & INFO_DRAIN) {
|
||||
for (int i = 0; i < 3; ++i)
|
||||
Number(line, e.v[i]);
|
||||
}
|
||||
if (e.info & INFO_EXTRA_SPELL) {
|
||||
const uint8_t *extra = SpellRecord(e.v[0]);
|
||||
Number(line, e.v[0]);
|
||||
Quoted(line, SpellName(extra));
|
||||
Number(line, static_cast<int>(SpellSchoolMask(extra)));
|
||||
}
|
||||
if (e.info & INFO_ITEM) {
|
||||
Number(line, e.v[0]);
|
||||
Quoted(line, e.itemNamed ? e.itemName.c_str() : nullptr);
|
||||
}
|
||||
if (e.info & INFO_AURA_TYPE)
|
||||
Plain(line, e.harmful ? "DEBUFF" : "BUFF");
|
||||
if (e.info & INFO_AURA_AMOUNT)
|
||||
Number(line, e.v[2]);
|
||||
return line;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Write(const Entry &entry) {
|
||||
// A line in the frame logging turned on comes before the frame's check.
|
||||
if (!Logging() || (g_log == 0 && !Open()))
|
||||
return;
|
||||
using Write_t = void(__cdecl *)(uint32_t log, const char *format, ...);
|
||||
reinterpret_cast<Write_t>(static_cast<uintptr_t>(Offsets::FUN_SLOG_WRITE))(
|
||||
g_log, "%s", Pack(entry).c_str());
|
||||
}
|
||||
|
||||
} // namespace CombatLog::File
|
||||
@@ -0,0 +1,29 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
// The combat log file — 4.3.4's WoWCombatLog.txt line, written while
|
||||
// LoggingCombat is on to a file per logging session named as retail names it
|
||||
// (Logs\WoWCombatLog-MMDDYY_HHMMSS.txt). 1.12's own WoWCombatLog.txt keeps
|
||||
// its chat-text lines.
|
||||
|
||||
#include "combatlog/Entry.h"
|
||||
|
||||
namespace CombatLog::File {
|
||||
|
||||
// SendCombatLogEvents' write: CombatLogEntry::PackEvent's line, through
|
||||
// CGChat::AddCombatMessage's SLogWrite, while combat logging is on.
|
||||
void Write(const Entry &entry);
|
||||
|
||||
} // namespace CombatLog::File
|
||||
@@ -55,6 +55,7 @@
|
||||
#include "aura/Data.h"
|
||||
#include "net/PacketDispatch.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "time/Clock.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -501,7 +502,7 @@ const uint8_t *ShieldSpell(uint64_t owner, uint32_t school) {
|
||||
const uint8_t *spell = SpellRecord(static_cast<int>(spellId));
|
||||
if (spell == nullptr ||
|
||||
Game::Read<uint32_t>(spell, Offsets::OFF_SPELL_RECORD_SCHOOL) != school ||
|
||||
!HasAuraEffect(spell, AURA_DAMAGE_SHIELD))
|
||||
!Spell::Lookup::HasAuraEffect(spell, AURA_DAMAGE_SHIELD))
|
||||
continue;
|
||||
if (found != nullptr && found != spell)
|
||||
return nullptr;
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "compat/WowTurbo.h"
|
||||
|
||||
#include "Common.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <windows.h>
|
||||
|
||||
namespace Compat::WowTurbo {
|
||||
|
||||
namespace {
|
||||
|
||||
// The prologue patch wow_turbo's MinHook writes on x86: `jmp rel32` over the
|
||||
// first five bytes. 1.12's entries open with `push ebp; mov ebp,esp`, never
|
||||
// the `mov edi,edi` that would make MinHook hot-patch instead.
|
||||
constexpr uint8_t kJmp = 0xE9;
|
||||
constexpr size_t kJmpLen = 5;
|
||||
|
||||
// The `kJmpLen` bytes at `va` as WoW.exe holds them on disk, located through
|
||||
// the loaded image's own section table.
|
||||
bool ReadStock(uintptr_t va, uint8_t (&out)[kJmpLen]) {
|
||||
const auto base = reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr));
|
||||
const auto *dos = reinterpret_cast<const IMAGE_DOS_HEADER *>(base);
|
||||
const auto *nt = reinterpret_cast<const IMAGE_NT_HEADERS *>(base + dos->e_lfanew);
|
||||
const IMAGE_SECTION_HEADER *section = IMAGE_FIRST_SECTION(nt);
|
||||
const uintptr_t rva = va - base;
|
||||
for (WORD i = 0; i < nt->FileHeader.NumberOfSections; ++i, ++section) {
|
||||
if (rva < section->VirtualAddress ||
|
||||
rva + kJmpLen > section->VirtualAddress + section->SizeOfRawData)
|
||||
continue;
|
||||
wchar_t path[MAX_PATH];
|
||||
const DWORD len = GetModuleFileNameW(nullptr, path, MAX_PATH);
|
||||
if (len == 0 || len == MAX_PATH)
|
||||
return false;
|
||||
const HANDLE file = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, nullptr,
|
||||
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (file == INVALID_HANDLE_VALUE)
|
||||
return false;
|
||||
OVERLAPPED at{};
|
||||
at.Offset = static_cast<DWORD>(rva - section->VirtualAddress + section->PointerToRawData);
|
||||
DWORD read = 0;
|
||||
const bool ok = ReadFile(file, out, kJmpLen, &read, &at) != 0 && read == kJmpLen;
|
||||
CloseHandle(file);
|
||||
return ok;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Reclaim(uintptr_t function) {
|
||||
const HMODULE turbo = GetModuleHandleW(L"wow_turbo.dll");
|
||||
if (turbo == nullptr)
|
||||
return;
|
||||
const auto *entry = reinterpret_cast<const uint8_t *>(function);
|
||||
if (entry[0] != kJmp)
|
||||
return;
|
||||
int32_t rel = 0;
|
||||
std::memcpy(&rel, entry + 1, sizeof rel);
|
||||
HMODULE owner = nullptr;
|
||||
if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
|
||||
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||
reinterpret_cast<LPCWSTR>(function + kJmpLen + rel), &owner) ||
|
||||
owner != turbo)
|
||||
return;
|
||||
uint8_t stock[kJmpLen];
|
||||
if (ReadStock(function, stock) && stock[0] != kJmp)
|
||||
Common::PatchBytes(reinterpret_cast<void *>(function), stock, kJmpLen);
|
||||
}
|
||||
|
||||
} // namespace Compat::WowTurbo
|
||||
@@ -0,0 +1,44 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// wow_turbo (github.com/athei/wow-mods) detours ~380 engine functions to Rust
|
||||
// reimplementations and yields any function another mod has already hooked:
|
||||
// its installer refuses a prologue that fails its signature. It installs from
|
||||
// DllMain, but ClassicAPI installs from `Load`, which VanillaFixes calls after
|
||||
// every DllMain, so wow_turbo never sees our hooks and always installs first.
|
||||
// MinHook then copies wow_turbo's jump into our trampoline, and our hook calls
|
||||
// the Rust body instead of the engine's. That loses everything that lives in
|
||||
// the stock body: Frame::ScriptArgs' pcall redirects in both script runners,
|
||||
// and InlineTexture / IndentedWordWrap in the text builder, whose Rust copy
|
||||
// calls its own emitter.
|
||||
//
|
||||
// `Reclaim` gives the stock prologue back before we hook, the state wow_turbo
|
||||
// itself produces when it loads after a mod that hooked first. wow_turbo keeps
|
||||
// its original in place, logs "prologue overwritten ... by ClassicAPI.dll"
|
||||
// and does not re-patch these entries.
|
||||
//
|
||||
// The DllMain core hooks need no reclaim: the only one wow_turbo also
|
||||
// replaces is the integrity scan, which both make the same no-op.
|
||||
|
||||
namespace Compat::WowTurbo {
|
||||
|
||||
// Restores the engine's own first instruction at `function` when it is a
|
||||
// `jmp` into wow_turbo.dll. No-op when wow_turbo is not loaded or the
|
||||
// prologue is anything else.
|
||||
void Reclaim(uintptr_t function);
|
||||
|
||||
} // namespace Compat::WowTurbo
|
||||
@@ -145,12 +145,8 @@ void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("ConsoleGetAllCommands", &Script_GetAllCommands);
|
||||
Game::Lua::RegisterGlobalFunction("ConsolePrintAllMatchingCommands",
|
||||
&Script_ConsolePrintAllMatchingCommands);
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "ConsoleCommandType", kCommandTypeEntries,
|
||||
sizeof(kCommandTypeEntries) /
|
||||
sizeof(kCommandTypeEntries[0]));
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "ConsoleCategory", kCategoryEntries,
|
||||
sizeof(kCategoryEntries) /
|
||||
sizeof(kCategoryEntries[0]));
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "ConsoleCommandType", kCommandTypeEntries);
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "ConsoleCategory", kCategoryEntries);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "console/Registry.h"
|
||||
#include "input/Modifier.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
@@ -63,7 +64,6 @@ namespace {
|
||||
using Handler_t = int(__fastcall *)(const void *data, void *param);
|
||||
using RegisterEx_t = void(__fastcall *)(int eventId, Handler_t handler, void *param,
|
||||
float priority);
|
||||
using IsKeyDown_t = int(__fastcall *)(int key);
|
||||
using GetInputLine_t = uintptr_t(__fastcall *)();
|
||||
using MakeCommandCurrent_t = void(__fastcall *)(uintptr_t line, const char *text);
|
||||
using SetInputString_t = void(__fastcall *)(const char *text);
|
||||
@@ -205,7 +205,7 @@ int __fastcall OnKeyDown(const void *data, void * /*param*/) {
|
||||
return 1;
|
||||
if (Game::Read<uint32_t>(Offsets::VAR_CONSOLE_VISIBLE) == 0)
|
||||
return 1;
|
||||
if (reinterpret_cast<IsKeyDown_t>(Offsets::FUN_GAMEUI_IS_KEY_DOWN)(toggleKey) != 0)
|
||||
if (Input::Modifier::IsKeyDown(toggleKey))
|
||||
return 1;
|
||||
|
||||
const uintptr_t line =
|
||||
|
||||
@@ -62,23 +62,9 @@ int __fastcall Script_C_Container_AutoStoreItem(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("srcBag", "BagIndex"),
|
||||
Game::Doc::Req("srcSlot", "luaIndex"),
|
||||
Game::Doc::Opt("dstBag", "BagIndex", "0",
|
||||
"0 for anywhere in the bags, 1 to 4 for one bag, -1 for the bank."),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Req("sent", "bool", "False when the server cannot be asked for this move."),
|
||||
};
|
||||
const Game::Doc::Function kAutoStoreItem{
|
||||
"Moves an item and lets the server choose where it goes, merging it into "
|
||||
"existing stacks first.",
|
||||
kArgs, kRets, nullptr, true};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "AutoStoreItem",
|
||||
&Script_C_Container_AutoStoreItem, &kAutoStoreItem);
|
||||
&Script_C_Container_AutoStoreItem);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "container/Bag.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "equipmentset/Locations.h"
|
||||
#include "item/BagFamily.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
namespace Container::Bag {
|
||||
|
||||
namespace {
|
||||
|
||||
bool PlayerRange(int first, int count, View *out) {
|
||||
auto *inv = Unit::Identity::PlayerInventoryManager();
|
||||
if (inv == nullptr)
|
||||
return false;
|
||||
*out = {inv, first, count, 0};
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// The main bank resolves only while a banker is open — 3.3.5 gates it on
|
||||
// `CGBankInfo::m_banker` the same way. Its GUIDs sit in the player's list
|
||||
// from login, but the server refuses every bank move without a banker and
|
||||
// the engine's own slot reader (`0x6228a0`) hides slots 39..68 on the same
|
||||
// banker-GUID test. Bank bags get that gate from `BagContainer`, whose
|
||||
// engine GUID getter answers 0 for them while the bank is closed.
|
||||
bool ViewFor(int bagID, View *out) {
|
||||
if (bagID == Offsets::BAG_INDEX_BACKPACK)
|
||||
return PlayerRange(Offsets::INVMGR_BACKPACK_FIRST_SLOT,
|
||||
Offsets::BACKPACK_NUM_SLOTS, out);
|
||||
if (bagID == Offsets::BAG_INDEX_BANK) {
|
||||
if (!EquipmentSet::Locations::BankerOpen())
|
||||
return false;
|
||||
return PlayerRange(Offsets::INVMGR_BANK_MAIN_FIRST_SLOT,
|
||||
Offsets::INVMGR_BANK_MAIN_LAST_SLOT -
|
||||
Offsets::INVMGR_BANK_MAIN_FIRST_SLOT + 1,
|
||||
out);
|
||||
}
|
||||
|
||||
const uint8_t *bag = Item::Location::BagContainer(bagID);
|
||||
auto *inv = static_cast<const uint8_t *>(Item::Location::ContainerInventory(bag));
|
||||
if (inv == nullptr)
|
||||
return false;
|
||||
const int slots = static_cast<int>(
|
||||
Game::Read<uint32_t>(inv, Offsets::OFF_INVMGR_SLOT_COUNT));
|
||||
if (slots <= 0)
|
||||
return false;
|
||||
|
||||
// The bag's own family, as 3.3.5's `CGItem_C::GetBagFamily` reads it (0
|
||||
// while its record isn't cached). `BitmaskForRecord` supplies the family
|
||||
// a vanilla bag's empty field leaves out, so a quiver reports one rather
|
||||
// than reading as general-purpose, and agrees with `C_Item.GetItemFamily`.
|
||||
uint32_t family = 0;
|
||||
const int bagItemID = Item::ID::FromCGItem(bag);
|
||||
if (bagItemID > 0) {
|
||||
if (auto *rec = Item::PeekRecord(static_cast<uint32_t>(bagItemID)))
|
||||
family = Item::BagFamily::BitmaskForRecord(rec);
|
||||
}
|
||||
*out = {inv, 0, slots, family};
|
||||
return true;
|
||||
}
|
||||
|
||||
uint64_t SlotGuid(const View &view, int slotIndex0) {
|
||||
const uint32_t linear = static_cast<uint32_t>(view.firstLinear + slotIndex0);
|
||||
if (linear >= Game::Read<uint32_t>(view.invMgr, Offsets::OFF_INVMGR_SLOT_COUNT))
|
||||
return 0;
|
||||
auto *guids = Game::Read<const uint64_t *>(view.invMgr, Offsets::OFF_INVMGR_GUID_ARRAY);
|
||||
return guids != nullptr ? guids[linear] : 0;
|
||||
}
|
||||
|
||||
int CountFree(const View &view) {
|
||||
if (view.invMgr != Unit::Identity::PlayerInventoryManager()) {
|
||||
using CountFreeSlots_t = uint32_t(__thiscall *)(const uint8_t *invMgr);
|
||||
auto countFreeSlots =
|
||||
reinterpret_cast<CountFreeSlots_t>(Offsets::FUN_INVMGR_COUNT_FREE_SLOTS);
|
||||
return static_cast<int>(countFreeSlots(view.invMgr));
|
||||
}
|
||||
int free = 0;
|
||||
for (int i = 0; i < view.slotCount; ++i) {
|
||||
if (SlotGuid(view, i) == 0)
|
||||
free++;
|
||||
}
|
||||
return free;
|
||||
}
|
||||
|
||||
} // namespace Container::Bag
|
||||
@@ -0,0 +1,47 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Container::Bag {
|
||||
|
||||
// A container's storage, addressed without Lua: the inventory list holding
|
||||
// its slot GUIDs, the linear index its first slot sits at, its slot count,
|
||||
// and the BagFamily bitmask it accepts (0 = anything). The backpack and the
|
||||
// main bank live inside the player's own inventory list at different linear
|
||||
// bases; equipped bags and bank bags each have a list of their own.
|
||||
struct View {
|
||||
const uint8_t *invMgr;
|
||||
int firstLinear;
|
||||
int slotCount;
|
||||
uint32_t family;
|
||||
};
|
||||
|
||||
// Resolves Lua container `bagID` — backpack 0, bags 1..4, main bank -1, bank
|
||||
// bags 5..10 — to its `View`. False for the keyring, an out-of-range ID, an
|
||||
// empty bag slot, and any bank container while no banker is open. Pure C — no
|
||||
// Lua stack.
|
||||
bool ViewFor(int bagID, View *out);
|
||||
|
||||
// GUID of the item in 0-based slot `slotIndex0` of `view`, 0 when empty.
|
||||
uint64_t SlotGuid(const View &view, int slotIndex0);
|
||||
|
||||
// Empty slots in `view`. A bag's own list is counted by the engine
|
||||
// (`FUN_INVMGR_COUNT_FREE_SLOTS`, 3.3.5's `CGContainer_C::GetFreeSlots`); a
|
||||
// range of the player's list (backpack, main bank) is walked slot by slot,
|
||||
// the way 3.3.5's `Script_GetContainerNumFreeSlots` walks it.
|
||||
int CountFree(const View &view);
|
||||
|
||||
} // namespace Container::Bag
|
||||
@@ -0,0 +1,52 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `Enum.BagIndex` — retail's BagIndexConstants enumeration, with Classic
|
||||
// Era's names for the containers this client has and this client's own
|
||||
// indices for them. Era numbers its bank bags from 6 because its reagent bag
|
||||
// takes 5; here they start right after the equipped bags, as the stock bank
|
||||
// frame numbers them. The reagent bag, reagent bank and account bank tabs
|
||||
// have no entry.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
namespace Container::BagIndex {
|
||||
|
||||
namespace {
|
||||
|
||||
const Game::Lua::EnumIntegerEntry kEntries[] = {
|
||||
{"Keyring", Offsets::BAG_INDEX_KEYRING},
|
||||
{"Bank", Offsets::BAG_INDEX_BANK},
|
||||
{"Backpack", Offsets::BAG_INDEX_BACKPACK},
|
||||
{"Bag_1", Offsets::BAG_INDEX_BAG_FIRST},
|
||||
{"Bag_2", Offsets::BAG_INDEX_BAG_FIRST + 1},
|
||||
{"Bag_3", Offsets::BAG_INDEX_BAG_FIRST + 2},
|
||||
{"Bag_4", Offsets::BAG_INDEX_BAG_LAST},
|
||||
{"BankBag_1", Offsets::BAG_INDEX_BANK_BAG_FIRST},
|
||||
{"BankBag_2", Offsets::BAG_INDEX_BANK_BAG_FIRST + 1},
|
||||
{"BankBag_3", Offsets::BAG_INDEX_BANK_BAG_FIRST + 2},
|
||||
{"BankBag_4", Offsets::BAG_INDEX_BANK_BAG_FIRST + 3},
|
||||
{"BankBag_5", Offsets::BAG_INDEX_BANK_BAG_FIRST + 4},
|
||||
{"BankBag_6", Offsets::BAG_INDEX_BANK_BAG_LAST},
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "BagIndex", kEntries, "BagIndexConstants");
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Container::BagIndex
|
||||
@@ -43,21 +43,9 @@ int __fastcall Script_C_Container_GetContainerItemCharges(void *L) {
|
||||
L, Item::Location::ResolveBag(L, bagID, slotIndex));
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("containerIndex", "BagIndex"),
|
||||
Game::Doc::Req("slotIndex", "luaIndex"),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Opt("uses", "number", nullptr, "Nil when the slot is empty."),
|
||||
};
|
||||
const Game::Doc::Function kGetContainerItemCharges{
|
||||
"Returns the uses left on the item in a bag slot: its charges times its stack size.",
|
||||
kArgs, kRets, nullptr, true};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemCharges",
|
||||
&Script_C_Container_GetContainerItemCharges,
|
||||
&kGetContainerItemCharges);
|
||||
&Script_C_Container_GetContainerItemCharges);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -33,22 +33,9 @@ int __fastcall Script_C_Container_GetItemCooldown(void *L) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("itemID", "number"),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Req("startTime", "number",
|
||||
"When the cooldown started, in GetTime() seconds; 0 when none."),
|
||||
Game::Doc::Req("duration", "number", "Length in seconds; 0 when there is no cooldown."),
|
||||
Game::Doc::Req("enable", "number",
|
||||
"0 while the cooldown waits to start, as for a potion used in combat; else 1."),
|
||||
};
|
||||
const Game::Doc::Function kGetItemCooldown{
|
||||
"Returns the cooldown of an item's use effect.", kArgs, kRets};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetItemCooldown",
|
||||
&Script_C_Container_GetItemCooldown, &kGetItemCooldown);
|
||||
&Script_C_Container_GetItemCooldown);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -40,22 +40,9 @@ int __fastcall Script_C_Container_GetContainerItemDurability(void *L) {
|
||||
L, Item::Location::ResolveBag(L, bagID, slotIndex));
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("containerIndex", "BagIndex"),
|
||||
Game::Doc::Req("slotIndex", "luaIndex"),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Opt("durability", "number", nullptr,
|
||||
"Nil when the slot is empty or the item has no durability."),
|
||||
Game::Doc::Opt("maxDurability", "number"),
|
||||
};
|
||||
const Game::Doc::Function kGetContainerItemDurability{
|
||||
"Returns the current and maximum durability of the item in a bag slot.", kArgs, kRets};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemDurability",
|
||||
&Script_C_Container_GetContainerItemDurability,
|
||||
&kGetContainerItemDurability);
|
||||
&Script_C_Container_GetContainerItemDurability);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -80,25 +80,10 @@ int __fastcall Script_C_Container_GetContainerItemEquipmentSetInfo(void *L) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("containerIndex", "BagIndex"),
|
||||
Game::Doc::Req("slotIndex", "luaIndex"),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Req("inSet", "bool"),
|
||||
Game::Doc::Opt("setList", "string", nullptr,
|
||||
"The set names joined by PLAYER_LIST_DELIMITER; nil when inSet is false."),
|
||||
};
|
||||
const Game::Doc::Function kGetContainerItemEquipmentSetInfo{
|
||||
"Returns whether the item in a bag slot is saved in an equipment set, and the "
|
||||
"names of those sets.",
|
||||
kArgs, kRets};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction(
|
||||
"C_Container", "GetContainerItemEquipmentSetInfo",
|
||||
&Script_C_Container_GetContainerItemEquipmentSetInfo,
|
||||
&kGetContainerItemEquipmentSetInfo);
|
||||
&Script_C_Container_GetContainerItemEquipmentSetInfo);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user