mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-15 15:40:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb6df3556c | ||
|
|
d3f0674b1a | ||
|
|
4ab88523ba | ||
|
|
73266d64a2 | ||
|
|
0959f9ebf6 | ||
|
|
944236d6e6 | ||
|
|
35be7600b6 | ||
|
|
b7a78565f7 | ||
|
|
3bf4574132 | ||
|
|
a28efb73e5 | ||
|
|
8e721237f6 | ||
|
|
d689aab94c | ||
|
|
71206cad13 | ||
|
|
b208faec31 | ||
|
|
b0f4f68a0f | ||
|
|
960c510d51 | ||
|
|
6b907f09d4 | ||
|
|
8c4a9be0dd | ||
|
|
a16b99073a | ||
|
|
d7103de564 | ||
|
|
c00eb24a8d | ||
|
|
b65c9e0319 | ||
|
|
de033896b1 | ||
|
|
dc8d4caf12 | ||
|
|
20069f3815 | ||
|
|
f2fa112f0a | ||
|
|
fca967c827 | ||
|
|
24404b5a23 | ||
|
|
a106d86969 | ||
|
|
fac3be3a0b | ||
|
|
b49c107778 | ||
|
|
53b7f79440 | ||
|
|
7b21453469 | ||
|
|
ebfd9d034d | ||
|
|
d825bcbdda | ||
|
|
6b36bb10e6 | ||
|
|
6628dcca19 |
@@ -0,0 +1,667 @@
|
||||
-- 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.deathless, e.flying = nil, nil;
|
||||
BW.ResetActs(e); -- Foes.lua
|
||||
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);
|
||||
if e.kind.crackSound then
|
||||
e.actor:PlaySound(e.kind.crackSound);
|
||||
end
|
||||
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();
|
||||
if e.kind.xp > 0 then
|
||||
DropGem(x, y, z, e.kind.xp, e.kind.gemScale);
|
||||
end
|
||||
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
|
||||
if e.deathless then -- Onyxia in the air: she dies on the ground
|
||||
e.hp = 1;
|
||||
else
|
||||
Kill(e);
|
||||
end
|
||||
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,768 @@
|
||||
-- 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. At ten minutes
|
||||
-- the brood stops coming and Onyxia descends (Onyxia.lua): slay her 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
|
||||
if BW.run.fearUntil then
|
||||
-- Bellowing Roar's fear (Onyxia.lua): you run from her.
|
||||
BW.Drive(1, 0, BW.run.fearYaw);
|
||||
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(string.format("Victory! %s fell at %s, after a fight of %s", BW.BOSS.NAME,
|
||||
Clock(BW.run.boss.slainAt), Clock(BW.run.boss.slainAt - BW.run.boss.startTime)));
|
||||
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.EndRun = EndRun -- Onyxia.lua, when she falls
|
||||
|
||||
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.ClearOnyxia();
|
||||
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, until Onyxia comes; then only her whelps come (Onyxia.lua).
|
||||
if not BW.run.boss then
|
||||
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
|
||||
if BW.run.time >= RUN_SECONDS then
|
||||
BW.SummonOnyxia();
|
||||
end
|
||||
end
|
||||
UpdateEggs();
|
||||
UpdateMines(dt, hx, hy);
|
||||
UpdateVents(dt, hx, hy);
|
||||
BW.UpdateMarkers();
|
||||
|
||||
Chase(dt, hx, hy, hz);
|
||||
BW.UpdateFoes(dt, hx, hy, hz);
|
||||
BW.UpdateOnyxia(dt, hx, hy, hz);
|
||||
if BW.run.over then -- she fell
|
||||
return;
|
||||
end
|
||||
|
||||
-- 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 e.flying 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 and not e.kind.boss 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
|
||||
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.bossBar = getglobal(hud .. "Boss");
|
||||
BW.bossText = getglobal(hud .. "BossText");
|
||||
BW.alertText = getglobal(hud .. "BossAlert");
|
||||
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 onyxia [2|3]` with
|
||||
-- Onyxia coming at once (with 2 or 3, at the health that starts that
|
||||
-- phase: she lands, then takes to the air), `/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 == "onyxia" then
|
||||
local _, _, phase = string.find(msg, "(%d)");
|
||||
local shares = { ["2"] = BW.BOSS.PHASE_TWO - 0.01, ["3"] = BW.BOSS.PHASE_THREE - 0.01 };
|
||||
BW.SummonOnyxia(phase and shares[phase]);
|
||||
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,21 @@
|
||||
## 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
|
||||
Onyxia.lua
|
||||
Gear.lua
|
||||
Markers.lua
|
||||
Broodwatch.lua
|
||||
Broodwatch.xml
|
||||
@@ -0,0 +1,267 @@
|
||||
<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>
|
||||
<!-- Onyxia's health while she fights (Onyxia.lua): her name and what is
|
||||
left; under it, her announcements, which fade. -->
|
||||
<StatusBar name="$parentBoss" hidden="true">
|
||||
<Size x="420" y="18"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOP" x="0" y="-16"/>
|
||||
</Anchors>
|
||||
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
|
||||
<BarColor r="0.85" g="0.2" b="0.1"/>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture setAllPoints="true">
|
||||
<Color r="0" g="0" b="0" a="0.6"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
<Layer level="OVERLAY">
|
||||
<FontString name="$parentText" inherits="GameFontHighlight">
|
||||
<Anchors>
|
||||
<Anchor point="CENTER"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
<FontString name="$parentAlert" inherits="GameFontNormalLarge" hidden="true">
|
||||
<Anchors>
|
||||
<Anchor point="TOP" relativePoint="BOTTOM" x="0" y="-8"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
</StatusBar>
|
||||
<!-- 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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 and not BW.run.boss then -- Onyxia lays none
|
||||
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,469 @@
|
||||
-- Broodwatch foes: what the lair's brood does besides bite. A kind with
|
||||
-- `acts` (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.
|
||||
-- Onyxia (Onyxia.lua) uses the same acts, and adds her own.
|
||||
|
||||
local _, BW = ...
|
||||
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
|
||||
local MaxHp = BW.MaxHp
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- 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 by an act, of the act's school (Core.lua's HurtHero):
|
||||
-- `share` of your maximum health (Onyxia's blows), or `hit` seconds of the
|
||||
-- striker's bite, softened as a bite is (Demoralizing Shout's weakness).
|
||||
-- Devotion softens both. `breath`: the act is a dragon's breath.
|
||||
|
||||
local function Strike(e, act)
|
||||
local devotion = 1 - PASSIVES.devotion.per * (BW.run.passives.devotion or 0) / 100;
|
||||
local amount;
|
||||
if act.share then
|
||||
amount = MaxHp() * act.share;
|
||||
else
|
||||
amount = e.dps * act.hit * BiteShare(e);
|
||||
end
|
||||
BW.HurtHero(amount * devotion, act.school, act.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 Go(e, act) and Charge(e, act, dt, hx, hy) (a move
|
||||
-- after the wind-up, until Charge returns true; Go returns the most it
|
||||
-- takes), and Stop(e, act) (clears what Start or Go left, when the act ends
|
||||
-- or is broken off). A strike that comes with a spell's visual lands at the
|
||||
-- visual's impact (BW.OnImpact runs the in-flight entry's onImpact). The
|
||||
-- visuals are cast at a point: no actor is hit, so no actor plays an impact
|
||||
-- animation.
|
||||
|
||||
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
|
||||
|
||||
-- Registers a visual's flight: `entry.onImpact(entry)` runs at its impact,
|
||||
-- and its marks clear by `entry.expires` if the impact is lost.
|
||||
local function Track(e, castID, entry)
|
||||
entry.expires = BW.run.time + entry.act.cast + 3;
|
||||
entry.e = e;
|
||||
BW.run.inFlight[castID] = entry;
|
||||
e.actCast = castID;
|
||||
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, act = act };
|
||||
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;
|
||||
Track(e, castID, entry);
|
||||
return true;
|
||||
end,
|
||||
}
|
||||
|
||||
-- Whether (px, py) is within `length` of (x, y) and `arc` of `yaw`.
|
||||
local function InCone(px, py, x, y, yaw, length, arc)
|
||||
local dx, dy = px - x, py - y;
|
||||
if dx * dx + dy * dy > length * length then
|
||||
return false;
|
||||
end
|
||||
local off = math.mod(math.atan2(dy, dx) - yaw + 3 * math.pi, 2 * math.pi) - math.pi;
|
||||
return math.abs(off) <= arc / 2;
|
||||
end
|
||||
|
||||
-- A cone `length` long and `arc` wide: ahead, at you (a breath), or with
|
||||
-- `back`, behind as the striker faces (a tail's sweep, which does not turn).
|
||||
-- Three marks along it, each as wide as the cone where it lies. A strike
|
||||
-- with `knock` throws you away from the striker.
|
||||
acts.cone = {
|
||||
Start = function(e, act, hx, hy, hz)
|
||||
local yaw, ex, ey;
|
||||
if act.back then
|
||||
ex, ey = e.actor:GetPosition();
|
||||
e.actor:StopMoving();
|
||||
yaw = e.actor:GetYaw() + math.pi;
|
||||
else
|
||||
yaw, ex, ey = Face(e, hx, hy);
|
||||
end
|
||||
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, act = act };
|
||||
local half = math.tan(math.min(act.arc, 2.6) / 2); -- a wide arc's marks stay on the floor near it
|
||||
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 a = landed.act;
|
||||
if InCone(px, py, landed.x, landed.y, landed.yaw, a.length, a.arc) then
|
||||
Strike(landed.e, a);
|
||||
if a.knock then
|
||||
KnockHero(landed.x, landed.y, a.knock);
|
||||
end
|
||||
end
|
||||
end;
|
||||
Track(e, castID, entry);
|
||||
return true;
|
||||
end,
|
||||
}
|
||||
|
||||
-- A blow all around the striker, `radius` wide (a wing's buffet): one mark
|
||||
-- under it. A strike throws you away by `knock`.
|
||||
acts.nova = {
|
||||
Start = function(e, act, hx, hy, hz)
|
||||
local _, ex, ey = Face(e, hx, hy);
|
||||
local _, _, ez = e.actor:GetPosition();
|
||||
local castID = e.actor:PlaySpellVisualAtPoint(act.spell, ex, ey, ez, act.cast * 1000);
|
||||
if not castID then
|
||||
return false;
|
||||
end
|
||||
local entry = { marks = { Mark(ex, ey, ez, act.radius) }, x = ex, y = ey, act = act };
|
||||
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);
|
||||
KnockHero(landed.x, landed.y, landed.act.knock);
|
||||
end
|
||||
end;
|
||||
Track(e, castID, entry);
|
||||
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`, 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,
|
||||
-- A charge broken off stops where it is, at its own speed again.
|
||||
Stop = function(e)
|
||||
ApplySpeed(e);
|
||||
e.actor:StopMoving();
|
||||
end,
|
||||
}
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- An act's states on the enemy: e.actState is "wind" (it stands and shows
|
||||
-- its marks, `cast` seconds), "go" (a move after it), then "rest"
|
||||
-- (`recover` seconds; a thrown spell is out of its hands by then) and nil.
|
||||
-- e.act is the act under way. Each act has its own timer, e.actReady[act]:
|
||||
-- the run time it may start again, `every` seconds after it ends (plus up to
|
||||
-- `spread` more at random). A hold, a fear, a sheep, a taunt or a throw
|
||||
-- breaks a wind-up or a move off; what is already thrown still lands.
|
||||
--
|
||||
-- The acts an enemy picks from are e.acts (Onyxia's change with her phase),
|
||||
-- else its kind's; the first that is ready, and that you are in range of,
|
||||
-- starts.
|
||||
|
||||
local function Next(e, act, share)
|
||||
e.actReady[act] = BW.run.time + (act.every + math.random() * (act.spread or 0)) * share;
|
||||
end
|
||||
|
||||
local function EndMove(e)
|
||||
local stop = acts[e.act.kind].Stop;
|
||||
if stop then
|
||||
stop(e, e.act);
|
||||
end
|
||||
end
|
||||
|
||||
local function Break(e)
|
||||
local act = e.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
|
||||
end
|
||||
if e.actState ~= "rest" then
|
||||
EndMove(e); -- what the act's Start or Go left (a charge's speed, a Deep Breath's lane)
|
||||
end
|
||||
if e.actMarks then
|
||||
Unmark(e.actMarks);
|
||||
end
|
||||
e.actState, e.actCast, e.act = nil, nil, nil;
|
||||
Next(e, act, 0.5);
|
||||
end
|
||||
|
||||
-- Whether the act may start: you are within its `range` (no closer than
|
||||
-- `near`), and for an act with a `side`, on that side of the striker as it
|
||||
-- faces ("front" or "back", half the circle each).
|
||||
local function InRange(e, act, hx, hy)
|
||||
local x, y = e.actor:GetPosition();
|
||||
local d2 = Dist2(x, y, hx, hy);
|
||||
if d2 > act.range * act.range or (act.near and d2 < act.near * act.near) then
|
||||
return false;
|
||||
end
|
||||
if act.side then
|
||||
local off = math.mod(math.atan2(hy - y, hx - x) - e.actor:GetYaw() + 3 * math.pi, 2 * math.pi) - math.pi;
|
||||
return (math.abs(off) <= math.pi / 2) == (act.side == "front");
|
||||
end
|
||||
return true;
|
||||
end
|
||||
|
||||
-- Starts an act now, whatever its timer (Onyxia's Deep Breath, from her
|
||||
-- flight). Returns whether it started.
|
||||
local function StartAct(e, act, hx, hy, hz)
|
||||
if not acts[act.kind].Start(e, act, hx, hy, hz) then
|
||||
return false;
|
||||
end
|
||||
e.act, e.actState, e.actLeft = act, "wind", act.cast;
|
||||
return true;
|
||||
end
|
||||
|
||||
-- The first ready act in range starts. A fresh enemy waits a while for
|
||||
-- each act (`first` seconds, else up to 1.5 times its `every`), not in step
|
||||
-- with the rest.
|
||||
local function TryStart(e, list, hx, hy, hz)
|
||||
local t = BW.run.time;
|
||||
for _, act in ipairs(list) do
|
||||
local ready = e.actReady[act];
|
||||
if not ready then
|
||||
if act.first then
|
||||
e.actReady[act] = t + act.first;
|
||||
else
|
||||
Next(e, act, 0.5 + math.random());
|
||||
end
|
||||
elseif t >= ready and InRange(e, act, hx, hy) and StartAct(e, act, hx, hy, hz) then
|
||||
return;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function UpdateFoe(e, 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
|
||||
local list = e.acts or e.kind.acts;
|
||||
if list then
|
||||
e.actReady = e.actReady or {};
|
||||
TryStart(e, list, hx, hy, hz);
|
||||
end
|
||||
return;
|
||||
end
|
||||
local act = e.act;
|
||||
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
|
||||
EndMove(e);
|
||||
if e.actMarks then
|
||||
Unmark(e.actMarks);
|
||||
end
|
||||
e.actState, e.actLeft = "rest", act.recover;
|
||||
end
|
||||
elseif e.actLeft <= 0 then
|
||||
e.actState, e.act = nil, 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
|
||||
if (e.alive and (e.acts or e.kind.acts)) or e.actState then
|
||||
UpdateFoe(e, dt, hx, hy, hz);
|
||||
end
|
||||
end
|
||||
Expire();
|
||||
end
|
||||
|
||||
-- Whether the enemy is busy with an act (Chase leaves it be). Onyxia always
|
||||
-- is: she moves herself (Onyxia.lua).
|
||||
local function Acting(e)
|
||||
return e.actState ~= nil or e.kind.boss;
|
||||
end
|
||||
|
||||
-- A fresh enemy from the pool: no act under way, every act's timer fresh.
|
||||
local function ResetActs(e)
|
||||
e.actState, e.act, e.acts = nil, nil, nil;
|
||||
if e.actReady then
|
||||
for act in pairs(e.actReady) do
|
||||
e.actReady[act] = nil;
|
||||
end
|
||||
end
|
||||
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.ACTS = acts
|
||||
BW.Mark = Mark
|
||||
BW.Unmark = Unmark
|
||||
BW.Strike = Strike
|
||||
BW.KnockHero = KnockHero
|
||||
BW.Face = Face
|
||||
BW.Track = Track
|
||||
BW.Break = Break
|
||||
BW.StartAct = StartAct
|
||||
BW.UpdateFoes = UpdateFoes
|
||||
BW.Acting = Acting
|
||||
BW.ResetActs = ResetActs
|
||||
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,775 @@
|
||||
-- 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
|
||||
-- A spell spent on an evolution or fusion you own does not come back.
|
||||
local spent = {};
|
||||
for from, evolution in pairs(EVOLUTIONS) do
|
||||
if BW.run.spells[evolution.into] then
|
||||
spent[from] = true;
|
||||
end
|
||||
end
|
||||
for _, fusion in ipairs(FUSIONS) do
|
||||
if BW.run.spells[fusion.into] then
|
||||
spent[fusion.a] = true;
|
||||
spent[fusion.b] = true;
|
||||
end
|
||||
end
|
||||
for _, key in ipairs(SPELL_ORDER) do
|
||||
local state = BW.run.spells[key];
|
||||
if SPELLS[key].evolved or spent[key] then
|
||||
-- An evolution comes only from its spell (below), and has one rank;
|
||||
-- a spent spell is gone for the run.
|
||||
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,409 @@
|
||||
-- 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.boss then
|
||||
Damage(e, e.maxHp * TUNING.SAPPER_BOSS / BW.run.damageScale, TUNING.ITEM_COLOR);
|
||||
elseif 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 and not e.kind.boss;
|
||||
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
|
||||
@@ -0,0 +1,631 @@
|
||||
-- Broodwatch's Onyxia: at RUN_SECONDS the brood stops coming and its mother
|
||||
-- descends. Her death wins the run.
|
||||
--
|
||||
-- She fights in the world's three phases (Data.lua's BOSS): on the ground,
|
||||
-- in the air over the lair, and on the ground again. Her acts are Foes.lua's
|
||||
-- (Data.lua's ONYXIA_ACTS); this file moves her, changes her phase, sends her
|
||||
-- whelps and adds the two acts that are hers alone: Deep Breath and
|
||||
-- Bellowing Roar.
|
||||
--
|
||||
-- In the air her movement is off (an actor's movement walks the floor): her
|
||||
-- position eases along a straight line each step (FlyTo), with the model's
|
||||
-- own LiftOff, Fly, Hover and Land animations.
|
||||
|
||||
local _, BW = ...
|
||||
local ONYXIA = BW.ONYXIA
|
||||
local BOSS = BW.BOSS
|
||||
local CENTER = BW.CENTER
|
||||
local ROOM_RADIUS = BW.ROOM_RADIUS
|
||||
local MAX_ALIVE = BW.MAX_ALIVE
|
||||
local EASE = BW.EASE
|
||||
local AT_ONCE = BW.AT_ONCE
|
||||
local enemies = BW.enemies
|
||||
local Dist2 = BW.Dist2
|
||||
local FloorAt = BW.FloorAt
|
||||
local RunGrowth = BW.RunGrowth
|
||||
local Spawn = BW.Spawn
|
||||
local Kill = BW.Kill
|
||||
local Damage = BW.Damage
|
||||
local KindByKey = BW.KindByKey
|
||||
local ApplySpeed = BW.ApplySpeed
|
||||
local ACTS = BW.ACTS
|
||||
local Mark = BW.Mark
|
||||
local Unmark = BW.Unmark
|
||||
local Strike = BW.Strike
|
||||
local Track = BW.Track
|
||||
local StartAct = BW.StartAct
|
||||
local MarkVents = BW.MarkVents
|
||||
|
||||
-- DragonOnyxia.m2's sequences (AnimationData IDs).
|
||||
local ANIM_FLY = 135
|
||||
local ANIM_LIFTOFF = 192
|
||||
local ANIM_HOVER = 193
|
||||
local ANIM_LAND = 200
|
||||
local LIFT_SECONDS = 3 -- LiftOff and Land
|
||||
|
||||
local DEEP = BOSS.DEEP
|
||||
local ROAR = BOSS.ROAR
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- The bar over the scene: her name and health, with a tick at each phase's
|
||||
-- threshold; and an announcement under it, which fades.
|
||||
|
||||
local ticks -- the bar's phase ticks, made the first time it shows
|
||||
|
||||
local function ShowBar(e)
|
||||
local bar = BW.bossBar;
|
||||
if not ticks then
|
||||
ticks = {};
|
||||
for _, share in ipairs({ BOSS.PHASE_TWO, BOSS.PHASE_THREE }) do
|
||||
local tick = bar:CreateTexture(nil, "OVERLAY");
|
||||
tick:SetTexture(1, 1, 1, 0.6);
|
||||
tick:SetWidth(2);
|
||||
tick:SetHeight(bar:GetHeight());
|
||||
tick:SetPoint("LEFT", bar, "LEFT", bar:GetWidth() * share - 1, 0);
|
||||
tinsert(ticks, tick);
|
||||
end
|
||||
end
|
||||
bar:SetMinMaxValues(0, e.maxHp, AT_ONCE);
|
||||
bar:SetValue(e.hp, AT_ONCE);
|
||||
bar:Show();
|
||||
end
|
||||
|
||||
local shownHp
|
||||
|
||||
local function UpdateBar(e)
|
||||
local hp = math.max(e.hp, 0);
|
||||
if hp == shownHp then
|
||||
return;
|
||||
end
|
||||
shownHp = hp;
|
||||
BW.bossBar:SetValue(hp, EASE);
|
||||
BW.bossText:SetText(string.format("%s %d%%", BOSS.NAME, math.ceil(100 * hp / e.maxHp)));
|
||||
end
|
||||
|
||||
local alertLeft = 0
|
||||
|
||||
local function Alert(text)
|
||||
alertLeft = BOSS.ALERT_SECONDS;
|
||||
BW.alertText:SetText(text);
|
||||
BW.alertText:SetTextColor(BOSS.ALERT_COLOR[1], BOSS.ALERT_COLOR[2], BOSS.ALERT_COLOR[3]);
|
||||
BW.alertText:SetAlpha(1);
|
||||
BW.alertText:Show();
|
||||
end
|
||||
|
||||
local function UpdateAlert(dt)
|
||||
if alertLeft <= 0 then
|
||||
return;
|
||||
end
|
||||
alertLeft = alertLeft - dt;
|
||||
if alertLeft <= 0 then
|
||||
BW.alertText:Hide();
|
||||
elseif alertLeft < 1 then
|
||||
BW.alertText:SetAlpha(alertLeft);
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Places. The ring's point i (0 to RING_POINTS - 1) is HEIGHT over the
|
||||
-- floor at that height; she lands ARRIVE yards from you, toward CENTER.
|
||||
|
||||
local function RingPoint(i)
|
||||
local a = i * 2 * math.pi / BOSS.RING_POINTS;
|
||||
return CENTER[1] + BOSS.RING_RADIUS * math.cos(a), CENTER[2] + BOSS.RING_RADIUS * math.sin(a),
|
||||
CENTER[3] + BOSS.HEIGHT;
|
||||
end
|
||||
|
||||
local function NearestRing(x, y)
|
||||
local best, bestD2;
|
||||
for i = 0, BOSS.RING_POINTS - 1 do
|
||||
local rx, ry = RingPoint(i);
|
||||
local d2 = Dist2(x, y, rx, ry);
|
||||
if not best or d2 < bestD2 then
|
||||
best, bestD2 = i, d2;
|
||||
end
|
||||
end
|
||||
return best;
|
||||
end
|
||||
|
||||
local function LandingPoint()
|
||||
local hx, hy = BW.hero:GetPosition();
|
||||
local dx, dy = CENTER[1] - hx, CENTER[2] - hy;
|
||||
local d = math.sqrt(dx * dx + dy * dy);
|
||||
if d < 1 then
|
||||
dx, dy, d = 1, 0, 1;
|
||||
end
|
||||
local x, y = hx + dx / d * BOSS.ARRIVE, hy + dy / d * BOSS.ARRIVE;
|
||||
local cx, cy = x - CENTER[1], y - CENTER[2];
|
||||
local c = math.sqrt(cx * cx + cy * cy);
|
||||
if c > ROOM_RADIUS then
|
||||
x, y = CENTER[1] + cx / c * ROOM_RADIUS, CENTER[2] + cy / c * ROOM_RADIUS;
|
||||
end
|
||||
return x, y, FloorAt(x, y, CENTER[3]);
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Flight: a straight line from where she is to (x, y, z) in `seconds`, with
|
||||
-- `anim`, facing the way she goes; then done(boss).
|
||||
|
||||
local function FlyTo(boss, x, y, z, seconds, anim, done)
|
||||
local actor = boss.e.actor;
|
||||
local ax, ay, az = actor:GetPosition();
|
||||
boss.fly = { ax = ax, ay = ay, az = az, bx = x, by = y, bz = z, t = 0, seconds = math.max(seconds, 0.05), done = done };
|
||||
if Dist2(ax, ay, x, y) > 0.25 then
|
||||
actor:SetYaw(math.atan2(y - ay, x - ax));
|
||||
end
|
||||
actor:SetAnimation(anim);
|
||||
end
|
||||
|
||||
local function Distance(boss, x, y, z)
|
||||
local ax, ay, az = boss.e.actor:GetPosition();
|
||||
local dz = z - az;
|
||||
return math.sqrt(Dist2(ax, ay, x, y) + dz * dz);
|
||||
end
|
||||
|
||||
local function UpdateFlight(boss, dt)
|
||||
local fly = boss.fly;
|
||||
if not fly then
|
||||
return;
|
||||
end
|
||||
fly.t = math.min(fly.t + dt, fly.seconds);
|
||||
local k = fly.t / fly.seconds;
|
||||
boss.e.actor:SetPosition(fly.ax + (fly.bx - fly.ax) * k, fly.ay + (fly.by - fly.ay) * k, fly.az + (fly.bz - fly.az) * k);
|
||||
if fly.t >= fly.seconds then
|
||||
boss.fly = nil;
|
||||
fly.done(boss);
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Phases. While she changes phase (and while she flies to a new point) she
|
||||
-- does nothing else (e.acts nil), and nothing kills her in the air
|
||||
-- (e.deathless: Damage leaves her 1): she dies on the ground.
|
||||
|
||||
local Hover, Landed
|
||||
|
||||
local function Fight(boss, phase)
|
||||
local e = boss.e;
|
||||
boss.phase = phase;
|
||||
e.acts = BOSS.ACTS[phase];
|
||||
e.actReady = e.actReady or {};
|
||||
end
|
||||
|
||||
-- She stands where she is (her ring point), facing you, and throws fire.
|
||||
Hover = function(boss)
|
||||
local e = boss.e;
|
||||
e.actor:SetAnimation(ANIM_HOVER);
|
||||
Fight(boss, 2);
|
||||
e.actReady[BOSS.ACTS[2][1]] = BW.run.time + 2; -- a moment after she arrives, as the world's 5 s
|
||||
boss.nextMove = BW.run.time + BOSS.MOVE_EVERY + math.random() * BOSS.MOVE_SPREAD;
|
||||
if not boss.waved then
|
||||
boss.waved = true;
|
||||
boss.waveLeft = BOSS.WHELPS_FIRST;
|
||||
boss.nextWave = BW.run.time + BOSS.WAVE_EVERY;
|
||||
end
|
||||
end
|
||||
|
||||
local function Touchdown(boss)
|
||||
local x, y = boss.e.actor:GetPosition();
|
||||
FlyTo(boss, x, y, FloorAt(x, y, CENTER[3]), LIFT_SECONDS, ANIM_LAND, Landed);
|
||||
end
|
||||
|
||||
-- She comes down by you: Fly to just over the floor, then Land.
|
||||
local function Descend(boss)
|
||||
boss.e.acts = nil;
|
||||
boss.ring = nil;
|
||||
local x, y, z = LandingPoint();
|
||||
z = z + BOSS.LIFT_RISE;
|
||||
FlyTo(boss, x, y, z, Distance(boss, x, y, z) / BOSS.FLY_SPEED, ANIM_FLY, Touchdown);
|
||||
end
|
||||
|
||||
-- On the floor: her movement walks again. The first landing starts the
|
||||
-- fight; the second the last phase, with a roar at once.
|
||||
Landed = function(boss)
|
||||
local e = boss.e;
|
||||
e.actor:SetAnimation(BW.ANIM_STAND); -- as Spawn does: the walk takes over from Land
|
||||
e.actor:SetMovementEnabled(true);
|
||||
ApplySpeed(e);
|
||||
e.flying, e.deathless = nil, nil;
|
||||
if boss.phase == 2 then
|
||||
Fight(boss, 3);
|
||||
e.actReady[ROAR] = BW.run.time;
|
||||
boss.lateGap = BOSS.LATE_WHELP_EVERY;
|
||||
Alert(BOSS.NAME .. " lands!");
|
||||
else
|
||||
Fight(boss, 1);
|
||||
end
|
||||
end
|
||||
|
||||
local function Climb(boss)
|
||||
local x, y = boss.e.actor:GetPosition();
|
||||
boss.ring = NearestRing(x, y);
|
||||
local rx, ry, rz = RingPoint(boss.ring);
|
||||
FlyTo(boss, rx, ry, rz, Distance(boss, rx, ry, rz) / BOSS.FLY_SPEED, ANIM_FLY, Hover);
|
||||
end
|
||||
|
||||
local function LiftOff(boss)
|
||||
local e = boss.e;
|
||||
e.acts = nil;
|
||||
e.flying, e.deathless = true, true;
|
||||
e.actor:StopMoving();
|
||||
e.actor:SetMovementEnabled(false);
|
||||
local x, y, z = e.actor:GetPosition();
|
||||
FlyTo(boss, x, y, z + BOSS.LIFT_RISE, LIFT_SECONDS, ANIM_LIFTOFF, Climb);
|
||||
boss.phase = 2; -- on her way up
|
||||
Alert(BOSS.NAME .. " takes to the air!");
|
||||
end
|
||||
|
||||
-- In the air, at her point: to a neighbour, or across with Deep Breath.
|
||||
local function Move(boss, hx, hy, hz)
|
||||
local e = boss.e;
|
||||
e.acts = nil;
|
||||
if math.random() < BOSS.DEEP_CHANCE then
|
||||
boss.deepTo = math.mod(boss.ring + BOSS.RING_POINTS / 2, BOSS.RING_POINTS);
|
||||
if StartAct(e, DEEP, hx, hy, hz) then
|
||||
return;
|
||||
end
|
||||
end
|
||||
local step = math.random(2) == 1 and 1 or BOSS.RING_POINTS - 1;
|
||||
boss.ring = math.mod(boss.ring + step, BOSS.RING_POINTS);
|
||||
local rx, ry, rz = RingPoint(boss.ring);
|
||||
FlyTo(boss, rx, ry, rz, Distance(boss, rx, ry, rz) / BOSS.FLY_SPEED, ANIM_FLY, Hover);
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Deep Breath: she turns to the opposite point and draws breath (its 5 s
|
||||
-- cast) while the lane under her flight is marked, then flies across at the
|
||||
-- breath speed. Each mark bursts into burning ground as she passes over it:
|
||||
-- it burns you (once a breath) and the brood in it.
|
||||
|
||||
local areaPool = {}
|
||||
local areas = {} -- burning ground, until its time ends
|
||||
|
||||
local function BurnGround(x, y, z)
|
||||
local area = table.remove(areaPool) or { actor = BW.scene:AcquireActor() };
|
||||
area.actor:SetModelBySpellArea(DEEP.area, DEEP.width);
|
||||
area.actor:SetPosition(x, y, z);
|
||||
area.actor:Show();
|
||||
area.left = DEEP.areaSeconds;
|
||||
tinsert(areas, area);
|
||||
end
|
||||
|
||||
local function UpdateAreas(dt)
|
||||
for i = table.getn(areas), 1, -1 do
|
||||
local area = areas[i];
|
||||
area.left = area.left - dt;
|
||||
if area.left <= 0 then
|
||||
area.actor:Hide();
|
||||
tinsert(areaPool, area);
|
||||
table.remove(areas, i);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local lane = {} -- the marked points: { x, y, z, d (yards from the start), mark }
|
||||
|
||||
local function ClearLane()
|
||||
for i = table.getn(lane), 1, -1 do
|
||||
if lane[i].mark then
|
||||
Unmark({ lane[i].mark });
|
||||
end
|
||||
lane[i] = nil;
|
||||
end
|
||||
end
|
||||
|
||||
local function Breathe(e, point, hx, hy)
|
||||
Unmark({ point.mark });
|
||||
point.mark = nil;
|
||||
BurnGround(point.x, point.y, point.z);
|
||||
local r2 = DEEP.width * DEEP.width;
|
||||
if not BW.run.boss.breathHit and Dist2(hx, hy, point.x, point.y) <= r2 then
|
||||
BW.run.boss.breathHit = true;
|
||||
Strike(e, DEEP);
|
||||
end
|
||||
local damage = BOSS.DEEP_BROOD * RunGrowth();
|
||||
for _, o in ipairs(enemies) do
|
||||
if o.alive and o ~= e then
|
||||
local x, y = o.actor:GetPosition();
|
||||
if Dist2(x, y, point.x, point.y) <= r2 then
|
||||
Damage(o, damage, BOSS.ALERT_COLOR);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
ACTS.deep = {
|
||||
Start = function(e, act)
|
||||
local boss = BW.run.boss;
|
||||
local ax, ay = e.actor:GetPosition();
|
||||
local bx, by = RingPoint(boss.deepTo);
|
||||
local dx, dy = bx - ax, by - ay;
|
||||
local length = math.sqrt(dx * dx + dy * dy);
|
||||
local bz = FloorAt(bx, by, CENTER[3]);
|
||||
local castID = e.actor:PlaySpellVisualAtPoint(act.spell, bx, by, bz, act.cast * 1000);
|
||||
if not castID then
|
||||
return false;
|
||||
end
|
||||
e.actor:SetYaw(math.atan2(dy, dx));
|
||||
e.actCast = castID;
|
||||
for d = act.step / 2, length, act.step do
|
||||
local x, y = ax + dx / length * d, ay + dy / length * d;
|
||||
local z = FloorAt(x, y, CENTER[3]);
|
||||
tinsert(lane, { x = x, y = y, z = z, d = d, mark = Mark(x, y, z, act.width) });
|
||||
end
|
||||
boss.laneX, boss.laneY, boss.laneNext, boss.breathHit = ax, ay, 1, nil;
|
||||
Alert(BOSS.NAME .. " takes in a deep breath...");
|
||||
return true;
|
||||
end,
|
||||
Go = function(e)
|
||||
local boss = BW.run.boss;
|
||||
boss.ring = boss.deepTo;
|
||||
local rx, ry, rz = RingPoint(boss.ring);
|
||||
local seconds = Distance(boss, rx, ry, rz) / BOSS.BREATH_SPEED;
|
||||
FlyTo(boss, rx, ry, rz, seconds, ANIM_FLY, Hover);
|
||||
e.acts = nil; -- Hover gives them back when she arrives
|
||||
return seconds + 1;
|
||||
end,
|
||||
Charge = function(e, _, _, hx, hy)
|
||||
local boss = BW.run.boss;
|
||||
local x, y = e.actor:GetPosition();
|
||||
local flown = math.sqrt(Dist2(x, y, boss.laneX, boss.laneY));
|
||||
while lane[boss.laneNext] and (lane[boss.laneNext].d <= flown or not boss.fly) do
|
||||
Breathe(e, lane[boss.laneNext], hx, hy);
|
||||
boss.laneNext = boss.laneNext + 1;
|
||||
end
|
||||
return not lane[boss.laneNext] or e.actLeft <= 0;
|
||||
end,
|
||||
Stop = function()
|
||||
ClearLane();
|
||||
end,
|
||||
}
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Bellowing Roar: you flee from her for `fear` seconds (its Fear state on
|
||||
-- you; Steer runs you away), and `vents` of the lair's cracks are marked to
|
||||
-- erupt `warn` seconds after it, time to get clear once the fear ends.
|
||||
|
||||
local function FearHero(e, act)
|
||||
local run = BW.run;
|
||||
if run.graceUntil then
|
||||
return;
|
||||
end
|
||||
local hx, hy = BW.hero:GetPosition();
|
||||
local x, y = e.actor:GetPosition();
|
||||
if not run.fearUntil then
|
||||
BW.hero:AddAuraVisual(act.spell);
|
||||
end
|
||||
run.fearUntil = run.time + act.fear;
|
||||
run.fearYaw = math.atan2(hy - y, hx - x);
|
||||
run.fearSpell = act.spell;
|
||||
end
|
||||
|
||||
local function EndFear()
|
||||
local run = BW.run;
|
||||
if run.fearUntil then
|
||||
run.fearUntil = nil;
|
||||
BW.hero:RemoveAuraVisual(run.fearSpell);
|
||||
-- The hero still runs: Steer's next Drive stops it, or turns it the
|
||||
-- way the keys held say.
|
||||
end
|
||||
end
|
||||
|
||||
ACTS.roar = {
|
||||
Start = function(e, act)
|
||||
local x, y, z = e.actor:GetPosition();
|
||||
e.actor:StopMoving();
|
||||
local castID = e.actor:PlaySpellVisualAtPoint(act.spell, x, y, z, act.cast * 1000);
|
||||
if not castID then
|
||||
return false;
|
||||
end
|
||||
local entry = { marks = {}, act = act };
|
||||
entry.onImpact = function(landed)
|
||||
FearHero(landed.e, landed.act);
|
||||
MarkVents(landed.act.vents, landed.act.warn);
|
||||
end;
|
||||
Track(e, castID, entry);
|
||||
return true;
|
||||
end,
|
||||
}
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Her whelps: from the two caves, a pair at a time, in waves while she is in
|
||||
-- the air (the first larger); in the last phase one now and then. Black and
|
||||
-- red, as the brood is.
|
||||
|
||||
local function SendWhelp(cave)
|
||||
if BW.run.alive >= MAX_ALIVE then
|
||||
return;
|
||||
end
|
||||
local kind = KindByKey(math.random(2) == 1 and "black" or "red");
|
||||
Spawn(kind, cave[1], cave[2]);
|
||||
end
|
||||
|
||||
local function UpdateWhelps(boss, dt)
|
||||
local t = BW.run.time;
|
||||
if boss.phase == 2 and boss.nextWave and t >= boss.nextWave then
|
||||
boss.nextWave = boss.nextWave + BOSS.WAVE_EVERY;
|
||||
boss.waveLeft = boss.waveLeft + BOSS.WHELPS_WAVE + math.random(0, BOSS.WHELPS_SPREAD);
|
||||
end
|
||||
if boss.waveLeft > 0 then
|
||||
boss.waveGap = boss.waveGap - dt;
|
||||
if boss.waveGap <= 0 then
|
||||
boss.waveGap = BOSS.WHELP_GAP;
|
||||
for _, cave in ipairs(BOSS.CAVES) do
|
||||
SendWhelp(cave);
|
||||
end
|
||||
boss.waveLeft = boss.waveLeft - table.getn(BOSS.CAVES);
|
||||
end
|
||||
end
|
||||
if boss.phase == 3 then
|
||||
boss.lateGap = boss.lateGap - dt;
|
||||
if boss.lateGap <= 0 then
|
||||
boss.lateGap = BOSS.LATE_WHELP_EVERY + math.random() * BOSS.LATE_SPREAD;
|
||||
SendWhelp(BOSS.CAVES[math.random(table.getn(BOSS.CAVES))]);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- On the ground she closes to her reach. Standing, she turns to you while
|
||||
-- you are in front of her; behind her, she leaves you there, for her tail.
|
||||
|
||||
local function Chase(boss, dt, hx, hy, hz)
|
||||
local e = boss.e;
|
||||
boss.repath = boss.repath - dt;
|
||||
if boss.repath > 0 or e.actState then
|
||||
return;
|
||||
end
|
||||
boss.repath = BOSS.REPATH;
|
||||
local x, y = e.actor:GetPosition();
|
||||
local dx, dy = hx - x, hy - y;
|
||||
local d = math.sqrt(dx * dx + dy * dy);
|
||||
local stand = ONYXIA.reach - 1.5;
|
||||
if d > ONYXIA.reach - 1 then
|
||||
local k = (d - stand) / d;
|
||||
e.actor:MoveTo(x + dx * k, y + dy * k, hz);
|
||||
else
|
||||
e.actor:StopMoving();
|
||||
local yaw = math.atan2(dy, dx);
|
||||
local off = math.mod(yaw - e.actor:GetYaw() + 3 * math.pi, 2 * math.pi) - math.pi;
|
||||
if math.abs(off) <= math.pi / 2 then
|
||||
e.actor:SetYaw(yaw);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- In the air, standing at her point, she faces you.
|
||||
local function Watch(boss, dt, hx, hy)
|
||||
local e = boss.e;
|
||||
boss.repath = boss.repath - dt;
|
||||
if boss.repath > 0 or e.actState or boss.fly then
|
||||
return;
|
||||
end
|
||||
boss.repath = BOSS.REPATH;
|
||||
local x, y = e.actor:GetPosition();
|
||||
e.actor:SetYaw(math.atan2(hy - y, hx - x));
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Her death: the brood dies with her, nothing hurts you any more, she holds
|
||||
-- Death's last frame (her model has no Dead), and the run is won.
|
||||
|
||||
local VICTORY_AFTER = 1 -- seconds after she lies still
|
||||
|
||||
local function Slain(boss)
|
||||
local e = boss.e;
|
||||
boss.slainAt = BW.run.time;
|
||||
e.dying = math.huge; -- her corpse stays (Step would fade it)
|
||||
BW.run.graceUntil = math.huge;
|
||||
UpdateBar(e);
|
||||
EndFear();
|
||||
for _, o in ipairs(enemies) do
|
||||
if o.alive then
|
||||
Kill(o);
|
||||
end
|
||||
end
|
||||
Alert(BOSS.NAME .. " is slain!");
|
||||
end
|
||||
|
||||
local function UpdateDeath(boss)
|
||||
local since = BW.run.time - boss.slainAt;
|
||||
if not boss.still and since >= ONYXIA.crackSeconds then
|
||||
boss.still = true;
|
||||
boss.e.actor:SetPaused(true, false);
|
||||
end
|
||||
if since >= ONYXIA.crackSeconds + VICTORY_AFTER then
|
||||
BW.EndRun(true);
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- The fight's step (Broodwatch.lua's Step, after the brood's acts).
|
||||
|
||||
local function UpdateOnyxia(dt, hx, hy, hz)
|
||||
UpdateAlert(dt);
|
||||
UpdateAreas(dt);
|
||||
local boss = BW.run.boss;
|
||||
if not boss then
|
||||
return;
|
||||
end
|
||||
if BW.run.fearUntil and BW.run.time >= BW.run.fearUntil then
|
||||
EndFear();
|
||||
end
|
||||
local e = boss.e;
|
||||
if boss.slainAt then
|
||||
UpdateDeath(boss);
|
||||
return;
|
||||
end
|
||||
if not e.alive then
|
||||
Slain(boss);
|
||||
return;
|
||||
end
|
||||
UpdateBar(e);
|
||||
UpdateFlight(boss, dt);
|
||||
UpdateWhelps(boss, dt);
|
||||
local share = e.hp / e.maxHp;
|
||||
if boss.phase == 1 then
|
||||
if share < BOSS.PHASE_TWO and not e.actState then
|
||||
LiftOff(boss);
|
||||
else
|
||||
Chase(boss, dt, hx, hy, hz);
|
||||
end
|
||||
elseif boss.phase == 2 then
|
||||
if boss.fly or e.actState or not boss.ring then
|
||||
return; -- on the way, or busy
|
||||
end
|
||||
if share < BOSS.PHASE_THREE then
|
||||
Descend(boss);
|
||||
elseif BW.run.time >= boss.nextMove then
|
||||
Move(boss, hx, hy, hz);
|
||||
else
|
||||
Watch(boss, dt, hx, hy);
|
||||
end
|
||||
elseif boss.phase == 3 then
|
||||
Chase(boss, dt, hx, hy, hz);
|
||||
end
|
||||
end
|
||||
|
||||
-- She comes: from the ring's point nearest where she will land, she flies
|
||||
-- down to it and lands. `share` (a test's) starts her with that share of
|
||||
-- her health.
|
||||
local function SummonOnyxia(share)
|
||||
local x, y, z = LandingPoint();
|
||||
local e = Spawn(ONYXIA, x, y);
|
||||
e.actor:SetMovementEnabled(false);
|
||||
e.actor:SetNameText(BOSS.NAME, UnitReactionColor[2].r, UnitReactionColor[2].g, UnitReactionColor[2].b);
|
||||
e.flying, e.deathless = true, true;
|
||||
if share then
|
||||
e.hp = math.floor(e.maxHp * share);
|
||||
end
|
||||
local boss = { e = e, phase = 0, repath = 0, waveLeft = 0, waveGap = 0, lateGap = 0, startTime = BW.run.time };
|
||||
BW.run.boss = boss;
|
||||
local rx, ry, rz = RingPoint(NearestRing(x, y));
|
||||
e.actor:SetPosition(rx, ry, rz);
|
||||
z = z + BOSS.LIFT_RISE;
|
||||
FlyTo(boss, x, y, z, Distance(boss, x, y, z) / BOSS.FLY_SPEED, ANIM_FLY, Touchdown);
|
||||
shownHp = nil;
|
||||
ShowBar(e);
|
||||
UpdateBar(e);
|
||||
Alert(BOSS.NAME .. " descends!");
|
||||
end
|
||||
|
||||
-- A new run: no bar, no burning ground, no lane (ClearFoes takes the marks,
|
||||
-- ClearEnemies her).
|
||||
local function ClearOnyxia()
|
||||
BW.bossBar:Hide();
|
||||
BW.alertText:Hide();
|
||||
alertLeft = 0;
|
||||
for i = table.getn(areas), 1, -1 do
|
||||
areas[i].actor:Hide();
|
||||
tinsert(areaPool, areas[i]);
|
||||
areas[i] = nil;
|
||||
end
|
||||
for i = table.getn(lane), 1, -1 do
|
||||
lane[i] = nil;
|
||||
end
|
||||
end
|
||||
|
||||
-- For the files that load after this one.
|
||||
BW.SummonOnyxia = SummonOnyxia
|
||||
BW.UpdateOnyxia = UpdateOnyxia
|
||||
BW.ClearOnyxia = ClearOnyxia
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,130 @@
|
||||
-- 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
|
||||
|
||||
-- Marks `size` idle cracks for `warn` seconds (Onyxia's roar marks more,
|
||||
-- and longer: it fears you first).
|
||||
local function MarkVents(size, warn)
|
||||
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 = 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 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)
|
||||
MarkVents(size, TUNING.VENT_WARN);
|
||||
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
|
||||
BW.MarkVents = MarkVents
|
||||
@@ -133,6 +133,17 @@ target_include_directories(
|
||||
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME "ClassicAPI")
|
||||
|
||||
# Compile every source file as its own parallel job (MSBuild's MultiToolTask).
|
||||
# Offsets.h and Game.h reach nearly every file, so an edit to either rebuilds
|
||||
# the whole target. Without this, MSBuild compiles a project's files serially.
|
||||
# /MP alone does not fix it here: /MP works only inside one batch of files with
|
||||
# identical settings, and CMake gives each .cpp whose name repeats in another
|
||||
# directory (21 Info.cpp, 6 Tooltip.cpp, ...) its own object path, so ~130
|
||||
# files fell into one-file batches that ran one after another. Measured on a
|
||||
# 24-core machine: a full rebuild went from 81 s (serial) to 38 s (/MP) to
|
||||
# 12 s (MultiToolTask).
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES VS_GLOBAL_UseMultiToolTask true)
|
||||
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MT")
|
||||
# /O1 optimizes for size: the code is mostly short glue around engine calls,
|
||||
# and /O1 made the DLL 280 KB smaller than /O2 /Ob2 (link-time code generation,
|
||||
|
||||
@@ -11,29 +11,31 @@ self-contained.
|
||||
|
||||
## Highlights
|
||||
|
||||
The headline features. Each one takes effect as soon as the DLL loads, so your
|
||||
existing addons benefit without any code changes. The complete per-function
|
||||
reference is in **[docs/API.md](docs/API.md)**.
|
||||
The headline features. The complete per-function reference is in
|
||||
**[docs/API.md](docs/API.md)**.
|
||||
|
||||
| Feature | Effect |
|
||||
|---------|--------|
|
||||
| Lua 5.1 code | Addon code written for Lua 5.1 runs on the 1.12 Lua 5.0 VM. It covers the [5.1 syntax](docs/API.md#lua-51-syntax) (`#`, `%`, `...` expressions, `0x` hex, `[=[ ]=]` long brackets), [method calls on strings](docs/API.md#string-methods-supper-sformat) (`msg:match("^!(%w+)")`), [up to 60 upvalues](docs/API.md#upvalue-limit) for each function, and [new `for` loop variables](docs/API.md#for-loop-variables-per-pass) on each pass. Each addon file also gets its `(name, table)` through `...`. |
|
||||
| [Modern script handlers](docs/API.md#setmodernscriptargsenable--getmodernscriptargs) | Frame scripts get their values as arguments, for example `OnClick(self, button, down)` and `OnEvent(self, event, ...)`. [XML script bodies](docs/API.md#xml-script-bodies-see-self-and-their-arguments) such as `<OnLoad>self:Init()</OnLoad>` see `self` and their arguments by name. The `this` / `arg1` globals stay set. `SetModernScriptArgs(false)` restores the vanilla behavior. |
|
||||
| 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. |
|
||||
| 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. 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. |
|
||||
| [Focus target](docs/API.md#focus) | A sticky focus unit. `FocusUnit("target")` pins it and `ClearFocus()` drops it, with `PLAYER_FOCUS_CHANGED` on every change. The `focus` / `focustarget` tokens resolve with every `UnitX` function and fire unit events (`UNIT_HEALTH`, `UNIT_AURA`, … as `arg1 == "focus"`), and the predefined `FOCUSTARGET` / `TARGETFOCUS` keybinds are ready to set. |
|
||||
| [Combat log events](docs/API.md#combat_log_event_unfiltered--combat_log_event-events) | `COMBAT_LOG_EVENT_UNFILTERED` and `CombatLogGetCurrentEventInfo` give each combat event with GUIDs and `COMBATLOG_OBJECT_*` flags. The values do not depend on the client language, so damage meters do not need to parse chat text. `COMBAT_LOG_EVENT` fires for the events that pass the `CombatLogAddFilter` filters. |
|
||||
| [Retail-like `/reload`](docs/API.md#reload-picks-up-new-addons-and-new-files) | `/reload` picks up addon changes made while the game runs. A new folder under `Interface\AddOns\` registers and loads as a normal addon, new files added to an existing addon's TOC load, a newly installed addon's first SavedVariables save survives `/reload`, `##` metadata edits (`## SavedVariables:`, `## Dependencies:`, `## Title:`, …) take effect, and a deleted addon folder drops from the addon list. |
|
||||
| [Multi-flavor & conditional TOC](docs/API.md#conditional-and-multi-flavor-toc-loading) | Loads modern multi-flavor addons that ship one folder. Selects a version-specific TOC (`<Name>_ClassicAPI.toc` or `<Name>_Turtle.toc`) and the matching keybinding file (`Bindings_ClassicAPI.xml` / `Bindings_Turtle.xml`), accepts a comma-separated `## Interface:` version list (compatible when it includes the client version `11200`), and honors per-line `[AllowLoadGameType]` / `[AllowLoadTextLocale]` conditions and `[Family]` / `[Game]` / `[TextLocale]` path variables inside a TOC. |
|
||||
| [Launch switches](docs/API.md#launch) | Options you add to the shortcut that starts the game. `-config <name>` reads and writes `WTF\<name>` in place of `WTF\Config.wtf`, so one install can hold several settings profiles. `-gluescript` and `-gluescriptFile` run Lua at the login and character-select screens, every time those screens appear. `-gamescript` and `-gamescriptFile` run it one time, after you enter the world. Each script runs after the interface has loaded, so it can call interface functions and use frames. |
|
||||
| [Addon memory and CPU usage](docs/API.md#profiling) | Addon managers and performance tools can show how much Lua memory and CPU time each addon uses. `GetAddOnMemoryUsage` gives the memory of each addon. When `scriptProfile` is `1`, the CPU functions give the time of each addon, function, frame, and event. With profiling off, the CPU functions cost nothing. |
|
||||
| [Any texture size](docs/API.md#texture-size-and-shape) | Textures load at any size and any shape. Width and height do not need to be powers of two, and there is no fixed upper limit. The only limit is the maximum texture size of your graphics card. This applies to every texture path: `SetTexture`, sprite sheets, inline textures, tooltips, and masks. The client allocates memory for a large texture only when an addon loads one, so small textures cost nothing extra. |
|
||||
| Lua 5.1 code | Addon code written for Lua 5.1 runs on the 1.12 Lua 5.0 VM. This includes the [5.1 syntax](docs/API.md#lua-51-syntax), [method calls on strings](docs/API.md#string-methods-supper-sformat), [more upvalues](docs/API.md#upvalue-limit) and [new `for` loop variables](docs/API.md#for-loop-variables-per-pass) on each pass. |
|
||||
| [Retail-like `/reload`](docs/API.md#reload-picks-up-new-addons-and-new-files) | `/reload` loads new addon folders, new files in a TOC, and changes to `##` metadata such as `## SavedVariables:`. A deleted addon folder leaves the addon list. |
|
||||
| [Soft targeting](docs/API.md#soft-interact-softinteract--anyinteract) | Bind **Interact With Target** to a key to interact with the nearest NPC, corpse, chest or 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. |
|
||||
| [Macro conditions](docs/API.md#more-commands-with-conditions) | `[conditions]` and `@unit` work on 38 slash commands, from `/cast` and `/use` to `/target` and `/pet`. Addons read the same rules through `SecureCmdOptionParse`. |
|
||||
| [`#showtooltip`](docs/API.md#showtooltip-and-show) | A macro button shows the spell or item that its macro will use: icon, tooltip, cooldown, range and usable state. Action-bar addons get this with no change. |
|
||||
| Inline textures | Texture markup (`\|T…\|t`) and atlas markup (`\|A…\|a`) show as icons in font strings, chat and tooltips. `GetStringWidth` and `GetStringHeight` count the icons. |
|
||||
| [Model scenes](docs/API.md#modelscene) | Real 3D scenes inside the UI. Put a dungeon or a piece of the world around the models, and creatures or copies of players run, jump and swim through it, with a camera that follows them. This is enough to build a small game in the client. |
|
||||
| [Cooldown frames](docs/API.md#createframecooldown--name-parent-template) | `CreateFrame("Cooldown")` draws the modern cooldown sweep, edge and countdown numbers by itself. The addon does not need an `OnUpdate` script to animate it. |
|
||||
| [Timer bars](docs/API.md#statusbarsettimerdurationduration--interpolation--direction--statusbargettimerduration) | `statusBar:SetTimerDuration` fills a bar from a cast, channel or other duration, and `SetValue` can slide smoothly to a new value. The bar animates by itself, with no `OnUpdate` script. |
|
||||
| [Animations](docs/API.md#animation) | Animation groups on frames, textures and font strings, made in Lua or XML, with `Alpha`, `Translation`, `Scale`, `Rotation` and `Path` animations. |
|
||||
| [Event-driven nameplates](docs/API.md#nameplate) | The `C_NamePlate` API, with `NAME_PLATE_UNIT_ADDED` and `NAME_PLATE_UNIT_REMOVED` events. The `nameplateN` unit tokens work with every `UnitX` function and unit event. |
|
||||
| [Focus target](docs/API.md#focus) | `FocusUnit` sets a focus unit, `ClearFocus` clears it, and `PLAYER_FOCUS_CHANGED` fires on each change. The `focus` and `focustarget` tokens work with every `UnitX` function and unit event. |
|
||||
| [Combat log events](docs/API.md#combat_log_event_unfiltered--combat_log_event-events) | `COMBAT_LOG_EVENT_UNFILTERED` and `CombatLogGetCurrentEventInfo` give each combat event with GUIDs and flags. Damage meters do not need to parse chat text. |
|
||||
| [Modern script handlers](docs/API.md#setmodernscriptargsenable--getmodernscriptargs) | Frame scripts get their values as arguments, for example `OnClick(self, button, down)`. [XML script bodies](docs/API.md#xml-script-bodies-see-self-and-their-arguments) see `self` and their arguments by name. The `this` / `arg1` globals stay set. |
|
||||
| [Multi-flavor & conditional TOC](docs/API.md#conditional-and-multi-flavor-toc-loading) | Loads addons that ship one folder for many game versions. This includes `<Name>_ClassicAPI.toc` and `<Name>_Turtle.toc`, version lists in `## Interface:`, and conditions on TOC lines. |
|
||||
| [XML attributes](docs/API.md#parentkey-in-xml) | Modern XML works. This includes `parentKey`, `parentArray`, `mixin`, `<KeyValues>`, `inherits` with more than one template, `method` and `function` script handlers, and `intrinsic` frame types. Frames, textures, font strings and other widgets also take many new attributes. |
|
||||
| [Menus and dropdowns](docs/API.md#menus-and-dropdowns) | The modern menu system: `MenuUtil.CreateContextMenu` and `DropdownButton` build context menus and dropdowns with the retail API. |
|
||||
| Tooltip line cap | Removes the 30-line limit of `GameTooltip` and every other tooltip made from `GameTooltipTemplate`, so long tooltips keep all their lines. |
|
||||
| [Addon memory and CPU usage](docs/API.md#profiling) | `GetAddOnMemoryUsage` gives the Lua memory of each addon. When `scriptProfile` is `1`, the CPU functions give the time of each addon, function, frame and event. |
|
||||
| [Any texture size](docs/API.md#texture-size-and-shape) | Textures load at any width and height, not only powers of two. The only limit is the maximum texture size of the graphics card. |
|
||||
|
||||
Smaller behavior fixes, such as frame alpha and `SetParent` frame levels, are
|
||||
in **[docs/API.md](docs/API.md)** with the API that they change.
|
||||
@@ -57,7 +59,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
|
||||
| [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` |
|
||||
| [Bindings](docs/API.md#bindings) | `SetBindingSpell`, `SetBindingItem`, `SetBindingMacro`, `SetBindingClick`, `SetOverrideBinding`, `SetOverrideBindingSpell`, `SetOverrideBindingItem`, `SetOverrideBindingMacro`, `SetOverrideBindingClick`, `ClearOverrideBindings`, `GetBindingAction` (`checkOverride`), `GetBindingByKey`, `IsModifiedClick`, `GetModifiedClick`, `SetModifiedClick`, `GetNumModifiedClickActions`, `GetModifiedClickAction`, `C_KeyBindings.GetBindingIndex`, `C_KeyBindings.GetBindingContextForAction`, `C_KeyBindings.GetCustomBindingType` |
|
||||
| [Chat](docs/API.md#chat) | `GetCurrentChatEvent`, `GetCurrentChatGUID`, `IsChatAFK`, `IsChatDND`, `SendSystemMessage` |
|
||||
| [ChatBubbles](docs/API.md#chatbubbles) | `C_ChatBubbles.GetAllChatBubbles` |
|
||||
| [Class](docs/API.md#class) | `FillLocalizedClassList` |
|
||||
@@ -66,7 +68,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
|
||||
| [Combat](docs/API.md#combat) | `InCombatLockdown`, `StartAttack`, `StopAttack` |
|
||||
| [CombatLog](docs/API.md#combatlog) | `CombatLogGetCurrentEventInfo`, `COMBATLOG_OBJECT_*` (constants) |
|
||||
| [Console](docs/API.md#console) | `CalculateStringEditDistance`, `ConsoleAddMessage`, `ConsoleEcho`, `ConsoleExec`, `ConsoleGetAllCommands`, `ConsoleGetColorFromType`, `ConsoleGetFontHeight`, `ConsoleIsActive`, `ConsolePrintAllMatchingCommands`, `SetConsoleKey` |
|
||||
| [CVar](docs/API.md#cvar) | `C_CVar.AreCVarsLoaded`, `C_CVar.DoesCVarExist`, `C_CVar.GetCVarBitfield`, `C_CVar.GetCVarBool`, `C_CVar.GetCVarInfo`, `C_CVar.RemoveTempCVar`, `C_CVar.SetCVar`, `C_CVar.SetCVarBitfield`, `C_CVar.SetTempCVar`, `GetCVar`, `GetCVarDefault` |
|
||||
| [CVar](docs/API.md#cvar) | `C_CVar.AreCVarsLoaded`, `C_CVar.DoesCVarExist`, `C_CVar.GetCVar`, `C_CVar.GetCVarBitfield`, `C_CVar.GetCVarBool`, `C_CVar.GetCVarDefault`, `C_CVar.GetCVarInfo`, `C_CVar.RegisterCVar`, `C_CVar.RemoveTempCVar`, `C_CVar.SetCVar`, `C_CVar.SetCVarBitfield`, `C_CVar.SetTempCVar`, `GetCVar`, `GetCVarDefault` |
|
||||
| [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` |
|
||||
@@ -80,14 +82,16 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
|
||||
| [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`, `: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: 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`, `:SetMouseClickEnabled` / `:IsMouseClickEnabled`, `:SetMouseMotionEnabled` / `:IsMouseMotionEnabled`, `:IsMouseMotionFocus`, `:SetPropagateMouseClicks` / `:CanPropagateMouseClicks`, `:SetPropagateMouseMotion` / `:CanPropagateMouseMotion`, `:SetPassThroughButtons`, `: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`, `: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`, `:SetTimerDuration` / `:GetTimerDuration`, `:SetValue` / `:SetMinMaxValues` with `interpolation`, `:GetInterpolatedValue`, `:IsInterpolating`, `:SetToTargetValue` |
|
||||
| [Frame: Slider](docs/API.md#sliderenable--sliderdisable--sliderisenabled) | `:Enable` / `:Disable` / `:IsEnabled` |
|
||||
| [Frame: Slider](docs/API.md#sliderenable--sliderdisable--slidersetenabledenabled--sliderisenabled) | `:Enable` / `:Disable` / `:SetEnabled` / `: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` |
|
||||
@@ -96,7 +100,7 @@ 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` |
|
||||
@@ -112,7 +116,9 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
|
||||
| [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, hand, enchant glow, result), `DressUpModel:UndressSlot`, `DressUpModel:IsGeoReady`, `DressUpModel:SetSheathed` / `GetSheathed`, `DressUpModel:SetAutoDress` / `GetAutoDress`, `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` |
|
||||
| [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` |
|
||||
@@ -121,6 +127,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
|
||||
| [PlayerInfo](docs/API.md#playerinfo) | `C_PlayerInfo.CanUseItem`, `C_PlayerInfo.GetClass`, `C_PlayerInfo.GetName`, `C_PlayerInfo.GetRace`, `C_PlayerInfo.GetSex`, `C_PlayerInfo.GUIDIsCreature`, `C_PlayerInfo.GUIDIsGameObject`, `C_PlayerInfo.GUIDIsPet`, `C_PlayerInfo.GUIDIsPlayer`, `C_PlayerInfo.IsConnected` |
|
||||
| [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`) |
|
||||
| [SettingsUtil](docs/API.md#settingsutil) | `C_SettingsUtil.NotifySettingsLoaded`, `C_SettingsUtil.OpenSettingsPanel` |
|
||||
| [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`, `UnitCastingDuration`, `UnitChannelDuration` |
|
||||
| [SpellActivationOverlay](docs/API.md#spellactivationoverlay) | `C_SpellActivationOverlay.IsSpellOverlayed`, `IsSpellOverlayed` |
|
||||
@@ -161,6 +168,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` |
|
||||
|
||||
@@ -212,7 +220,8 @@ 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
|
||||
these settings, the player cache and the ClassicAPI key bindings in the game,
|
||||
open the game menu and click AddOn Options, then ClassicAPI.
|
||||
open the game menu and click AddOn Options. The Settings window opens on
|
||||
its ClassicAPI tab.
|
||||
|
||||
| Group | CVar | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
@@ -221,7 +230,7 @@ open the game menu and click AddOn Options, then ClassicAPI.
|
||||
| [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, talk to an NPC, use an object or an item, or open the flight map. |
|
||||
| | `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. |
|
||||
@@ -289,6 +298,7 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `COMBAT_LOG_EVENT` | `timestamp, subevent, hideCaster, sourceGUID, sourceName, sourceFlags, sourceRaidFlags, destGUID, destName, destFlags, destRaidFlags, ...` ([details](docs/API.md#combat_log_event_unfiltered--combat_log_event-events)) |
|
||||
| `COMBAT_LOG_EVENT_UNFILTERED` | same as `COMBAT_LOG_EVENT` |
|
||||
| `CURSOR_CHANGED` | `isDefault, newCursorType, oldCursorType, oldCursorVirtualID` |
|
||||
| `CVAR_UPDATE` | `cvarName, value` — fires for every cvar change ([details](docs/API.md#cvar_update-event-fires-for-every-change)) |
|
||||
| `DYNAMIC_GOSSIP_POI_UPDATED` | *(none)* |
|
||||
| `EQUIPMENT_SETS_CHANGED` | *(none)* |
|
||||
| `EQUIPMENT_SWAP_PENDING` | `setID` |
|
||||
@@ -304,7 +314,7 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `LOOT_HISTORY_ROLL_CHANGED` | `itemIndex, playerIndex` |
|
||||
| `LOOT_HISTORY_ROLL_COMPLETE` | `itemIndex` |
|
||||
| `LOOT_SCAN_COMPLETED` | *(none)* |
|
||||
| `LOSS_OF_CONTROL_ADDED` | `eventIndex` |
|
||||
| `LOSS_OF_CONTROL_ADDED` | `unitToken` (always `"player"`), `eventIndex` |
|
||||
| `LOSS_OF_CONTROL_UPDATE` | `unitToken` (always `"player"`) |
|
||||
| `MAP_EXPLORATION_UPDATED` | *(none)* |
|
||||
| `MODIFIER_STATE_CHANGED` | `keyName, down` |
|
||||
@@ -333,6 +343,8 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `QUEST_REMOVED` | `questID` |
|
||||
| `QUEST_TURNED_IN` | `questID, xpReward, moneyReward` |
|
||||
| `SELF_RES_SPELL_CHANGED` | *(none)* |
|
||||
| `SETTINGS_LOADED` | *(none)* |
|
||||
| `SETTINGS_PANEL_OPEN` | `openToCategoryID, scrollToElementName` |
|
||||
| `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) |
|
||||
@@ -372,6 +384,7 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `VOICE_CHAT_TTS_PLAYBACK_FINISHED` | `numConsumers, utteranceID, destination` |
|
||||
| `VOICE_CHAT_TTS_PLAYBACK_FAILED` | `status, utteranceID, destination` |
|
||||
| `VOICE_CHAT_TTS_VOICES_UPDATE` | *(none)* |
|
||||
| `WEAPON_ENCHANT_CHANGED` | *(none)* |
|
||||
| `WEAPON_SLOT_CHANGED` | *(none)* |
|
||||
| `WEAR_EQUIPMENT_SET` | `setID` |
|
||||
|
||||
@@ -387,6 +400,7 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| Item quality | `LE_ITEM_QUALITY_POOR` … `LE_ITEM_QUALITY_WOWTOKEN` |
|
||||
| Unit stat | `LE_UNIT_STAT_STRENGTH` … `LE_UNIT_STAT_SPIRIT` |
|
||||
| Addon security | `Enum.AddOnSecurityStatus.{Secure,Insecure,Banned,NotAvailable}` |
|
||||
| Binding set | `Enum.BindingSet.{Default,Account,Character,Current}` (0–3) |
|
||||
| Power type | `Enum.PowerType.{HealthCost,None,Mana,Rage,Focus,Energy,Happiness}` |
|
||||
| Inventory type | `Enum.InventoryType.Index*Type` (0–34, e.g. `IndexHeadType`=1 … `IndexRelicType`=28) |
|
||||
| Item class | `Enum.ItemClass.{Consumable,Container,Weapon,Gem,Armor,Reagent,Projectile,Tradegoods,ItemEnhancement,Recipe,Quiver,Questitem,Key,Miscellaneous,…}` (0–19) |
|
||||
@@ -398,6 +412,7 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| 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>
|
||||
@@ -471,11 +486,13 @@ functions but that consumer code still expects to find as globals —
|
||||
(`ContinueOnAddOnLoaded` and more), `FormattingUtil` (`GetMoneyString`,
|
||||
`FormatLargeNumber` and more), `FrameUtil`, `DataProvider`, `CircularBuffer`,
|
||||
`EasingUtil`, `ScriptAnimationUtil`, `Interpolator`, `EnumUtil`, `StringUtil`, `AnchorUtil`,
|
||||
`GridLayoutUtil`, and the layout frame templates (`VerticalLayoutFrame`,
|
||||
`GridLayoutUtil`, the layout frame templates (`VerticalLayoutFrame`,
|
||||
`HorizontalLayoutFrame`, `ResizeLayoutFrame`, `GridLayoutFrame`,
|
||||
`StaticGridLayoutFrame`). It also holds the conditional slash
|
||||
commands and `SecureCmdOptionParse`, the parser that reads `[conditions]`
|
||||
and `@unit` for them.
|
||||
`StaticGridLayoutFrame`), and the retail Settings window (`Settings`,
|
||||
`SettingsPanel`) with the `InterfaceOptions_AddCategory` functions on
|
||||
top. It also holds the conditional slash commands and
|
||||
`SecureCmdOptionParse`, the parser that reads `[conditions]` and `@unit`
|
||||
for them.
|
||||
|
||||
**You don't have to install the addon manually** — the DLL embeds
|
||||
the contents of [`src/embed/AddOns/!!!ClassicAPI/`](src/embed/AddOns/!!!ClassicAPI/) and
|
||||
|
||||
@@ -4135,3 +4135,143 @@ Larger, decide separately: mouse-motion / click split and propagation
|
||||
Line region type); texture slicing (`SetTextureSliceMargins` / `Mode`); Model
|
||||
camera / transform / alpha (~40 methods); `RegisterEventCallback`; EditBox
|
||||
secure-text and byte-limit options; MessageFrame fade power and message IDs.
|
||||
|
||||
## 107. LuaLS annotations from the doc descriptors — medium
|
||||
|
||||
Generate LuaLS / EmmyLua annotation files (`---@meta`) from the
|
||||
`Game::Doc` records, so editors get autocomplete and type checks for the
|
||||
whole ClassicAPI surface. Generate from the RECORDED registrations, never
|
||||
by parsing the C++ source: that keeps the files in step with what the DLL
|
||||
really registers.
|
||||
|
||||
Plan (Layer 1, end to end):
|
||||
- A read-only accessor in `api/Documentation.h` (entries, `SystemOf`,
|
||||
structures, sealed envs) so the writer is its own module.
|
||||
- `src/api/Annotations.cpp`: console command `ExportAPIAnnotations [dir]`
|
||||
(same pattern and `CreateFileA` write path as `interface/Export.cpp`;
|
||||
Lua can run it with `ConsoleExec`). Write ONE sorted file, so no stale
|
||||
per-system files stay behind. Refuse, or warn, until the game pass is
|
||||
sealed (export from the glue screen would miss every in-game entry).
|
||||
- Mapping: `bool` → `boolean`, `cstring` → `string`, `luaIndex` →
|
||||
`integer`; `Enum.*` and structure names pass through; Nilable or
|
||||
defaulted arg → `name?`; Vararg → `...`; structure → `---@class` +
|
||||
`---@field`; enum → `---@enum`. Extension functions and env-only
|
||||
functions get a note line. Undocumented entries become
|
||||
`---@param ... any` / `---@return any ...` stubs. Skip `_classicapi_*`.
|
||||
- Do NOT emit `string = {}` (and so on) for the Lua std-lib tables we
|
||||
extend (`coroutine math string table`); it would shadow LuaLS's
|
||||
built-ins. Check with LuaLS whether `function C_X.Y() end` alone defines
|
||||
the global `C_X`, or whether `C_X = {}` is needed.
|
||||
- Check with `lua-language-server --check`. LuaLS is installed at
|
||||
`~/.vscode/extensions/sumneko.lua-3.19.2-win32-x64/server/bin/`. Run it on
|
||||
Broodwatch and on one pfUI module; a deliberately wrong argument type
|
||||
must be flagged.
|
||||
- Commit the output to `annotations/` with a short `.luarc.json` example
|
||||
(`runtime.version = "Lua 5.1"`, the nearest mode, because LuaLS has no
|
||||
5.0).
|
||||
|
||||
Type names in the descriptors are Blizzard's own (`UnitToken`,
|
||||
`SpellIdentifier`, `WOWGUID`, `vector2`, `colorRGB`, `uiUnit`, `any`, ...),
|
||||
checked against `_forever_`'s `Blizzard_APIDocumentationGenerated` on
|
||||
2026-11-13. The mapping must cover each one: a Structure or enum name
|
||||
passes through, and a Blizzard alias needs its own row (`UnitToken` →
|
||||
`string`, `WOWGUID` → `string`, `uiUnit` → `number`, and so on). Every
|
||||
`Vararg` has a type in its first argument; the sentences are in the doc
|
||||
slot.
|
||||
|
||||
Later layers: frame methods as classes with inheritance (first check whether
|
||||
1.12's method-table setup chains child → parent registries; if not, keep a
|
||||
small parent table next to `kWidgetSystems`); an event-name `---@alias`
|
||||
plus payload docs; untyped stubs for the stock 1.12 globals (read from the
|
||||
engine's own registration batches, see `docs/BlizzardScriptAPI.md`).
|
||||
Ketho's `wow-api` extension is installed too. Check that its retail
|
||||
definitions do not shadow ours in a 1.12 workspace.
|
||||
|
||||
## 108. Developer tools — ideas, not scoped
|
||||
|
||||
- **Lua debugger for VS Code**: a Debug Adapter Protocol server in the
|
||||
DLL, built on the debug hook that `profile/` already installs. It gives
|
||||
breakpoints, stepping and locals in addon code.
|
||||
- **Hot reload**: embedded addons are junctioned, so the DLL can watch
|
||||
file times and re-run one changed file in its addon's environment
|
||||
without `/reload`.
|
||||
- **Packet inspector**: `Net::PacketDispatch` already sees every incoming
|
||||
opcode. Add outgoing CMSGs, per-opcode decoders, a filter, and export.
|
||||
- **Object manager browser**: every object with GUID, entry, type and
|
||||
position. Click one to watch any descriptor field live, with a graph.
|
||||
- **Frame inspector, like browser devtools**: an anchor-graph view, live
|
||||
property edits, drag to re-anchor, and the owning addon (the GC tags
|
||||
know it).
|
||||
- **Flame graphs**: sampled call stacks from the profiler, drawn on the
|
||||
same timeline as `/etrace`.
|
||||
- **In-client test runner**: a busted-style suite that runs at login, plus
|
||||
frame snapshots, gives ClassicAPI and addons regression tests.
|
||||
- **Generic DBC query API**: all 151 tables are catalogued in
|
||||
`docs/DBCs.md`; expose records with their schema
|
||||
(`C_DBC.GetRecord("Spell", id)` and iterators).
|
||||
- **Localhost bridge**: a small HTTP / WebSocket server in the DLL that
|
||||
streams events and data as JSON, for stream overlays, a second-screen
|
||||
dashboard, or an external REPL over the same pipe as the debugger.
|
||||
|
||||
## 109. Engine showpieces — ideas, not scoped
|
||||
|
||||
- **Encounter replay** in a ModelScene: see #110.
|
||||
- **World-space drawing**: world → screen projection plus ground height,
|
||||
so addons can draw circles, rings and lines on the real ground. Also a
|
||||
line-of-sight ray query (the engine already traces for camera collision
|
||||
and click-to-move). LOS and unit positions outside the group give a
|
||||
gameplay edge: check the server's addon policy before shipping them.
|
||||
- **Second world camera**: render the world pass again from another
|
||||
camera into a render target (a rear-view mirror, a spectator cam, a
|
||||
fixed "security camera"). Open question: can the world render run twice
|
||||
in one frame?
|
||||
- **ModelScene to texture**: a scene as a texture that any region (and
|
||||
any mask) can use, for live 3D portraits and nameplate heads.
|
||||
- **Photo / machinima mode**: freeze and pose actors, keyframe the camera
|
||||
with the animation `Path` type, then capture.
|
||||
- **Spell-visual browser**: play any SpellVisualKit from the DBC on a
|
||||
dummy actor.
|
||||
- **Client-side doodads**: place M2s in the world on this client only.
|
||||
|
||||
## 110. Advanced combat logging + encounter replay — large, layered
|
||||
|
||||
Two layers. The replay needs the log first.
|
||||
|
||||
**Layer 1: advanced combat logging (the `advancedCombatLogging` cvar).**
|
||||
Our combat log file (`combatlog/File.cpp`) writes 4.3.4's PackEvent line.
|
||||
Classic Era (1.15.7) writes `COMBAT_LOG_VERSION,9,ADVANCED_LOG_ENABLED,1,
|
||||
BUILD_VERSION,1.15.7,PROJECT_ID,2`, and it is the oracle: it has the same
|
||||
content and almost the same client knowledge. There are real Era logs at
|
||||
`C:\Program Files (x86)\World of Warcraft\_classic_era_\Logs\`.
|
||||
Observed (2026-11-12, BWL log `WoWCombatLog-052425_204021.txt`):
|
||||
- Timestamp `5/24/2025 20:40:21.890-5`: it includes the year and the UTC
|
||||
offset. Our SLogWrite stamp has neither.
|
||||
- Advanced block, inserted after the spell prefix on damage / heal /
|
||||
cast-success / energize / swing lines:
|
||||
`infoGUID, ownerGUID, curHP, maxHP, 0,0,0,0,0, -1, 0,0,0, X, Y, 0,
|
||||
facing, level`.
|
||||
In Era, the attack power / spell power / armor / absorb slots, the power
|
||||
slots (`-1,0,0,0`) and uiMapID (`0`) are EMPTY. Era fills only what a
|
||||
client can know, so a 1.12 DLL can match it almost field for field
|
||||
(HP from descriptors, position and facing from movement info, level).
|
||||
Era writes player HP as a percent (`93,100`) and creature HP as raw values.
|
||||
- The info unit depends on the event: SPELL_DAMAGE → the DEST,
|
||||
SWING_DAMAGE → the source, SWING_DAMAGE_LANDED → the dest. Map each
|
||||
subevent from the log before coding. Do not guess.
|
||||
- Other line types: `ZONE_CHANGE,469,"Blackwing Lair",9`,
|
||||
`ENCOUNTER_START,610,"Razorgore the Untamed",9,40,469,2`,
|
||||
`ENCOUNTER_END,…,1` and `COMBATANT_INFO` (stats, talents, gear, auras).
|
||||
Encounter IDs come from a DBC that 1.12 does not have, so how to detect
|
||||
an engage is OPEN. COMBATANT_INFO is possible only for players we can
|
||||
inspect.
|
||||
- If the format matches Era, existing classic log parsers may read
|
||||
Turtle logs. Not checked.
|
||||
|
||||
**Layer 2: replay in a ModelScene.** Density from the same log
|
||||
(Razorgore, 398 s, 57 units): median 150 position samples per unit, about
|
||||
one every 2.6 s, and at most about 2 per second. That is enough for a
|
||||
Warcraft Logs-style replay and too coarse for a smooth 3D one. OPEN: add
|
||||
a client-side position sampler (a ClassicAPI-only line type or a separate
|
||||
recording), or interpolate as WCL does. The viewer needs map geometry:
|
||||
there is already a WMO backdrop (ModelScene) and the `feat/scene-terrain`
|
||||
branch.
|
||||
|
||||
+4659
-2928
File diff suppressed because it is too large
Load Diff
+10
-2
@@ -31,8 +31,16 @@ build uses the MSVC fastcall convention. Each `TValue` is **16 bytes** here
|
||||
|--------|-----------------|---------------------------------------------|
|
||||
| `+0x08`| `top` | `TValue *` — top of stack (next free slot) |
|
||||
| `+0x0C`| `base` | `TValue *` — base of the current frame |
|
||||
| `+0x10`| `ci` | `CallInfo *` — current call info |
|
||||
| `+0x18`| `stack` | bottom of stack array |
|
||||
| `+0x10`| `l_G` | `global_State *` |
|
||||
| `+0x14`| `ci` | `CallInfo *` — current call info (`lua_getstack` `0x006FBAA0` reads it) |
|
||||
| `+0x1C`| `stack` | bottom of stack array |
|
||||
| `+0x20`| `stacksize` | |
|
||||
| `+0x28`| `base_ci` | `CallInfo *` — first call info; `ci == base_ci` when no function runs |
|
||||
|
||||
The GC counters are `global_State` fields, at `l_G` (`L+0x10`):
|
||||
|
||||
| Offset | Field | Notes |
|
||||
|--------|-----------------|---------------------------------------------|
|
||||
| `+0x24`| `gcthreshold` | bytes; `lua_setgcthreshold` writes here |
|
||||
| `+0x28`| `nblocks` | live byte count (read by `lua_getgccount`) |
|
||||
|
||||
|
||||
+7
-4
@@ -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();
|
||||
|
||||
+29
-16
@@ -30,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) {
|
||||
@@ -252,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.
|
||||
@@ -361,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
|
||||
@@ -431,17 +445,16 @@ static bool SetIntegerTable(const char *parent, const char *sub,
|
||||
return true;
|
||||
}
|
||||
|
||||
void RegisterIntegerEnum(const char *parent, const char *sub,
|
||||
const EnumIntegerEntry *entries, int count,
|
||||
void RegisterIntegerEnum(const char *parent, const char *sub, ArrayRef<EnumIntegerEntry> entries,
|
||||
const char *docSystem) {
|
||||
if (SetIntegerTable(parent, sub, entries, count))
|
||||
Api::Documentation::RecordEnum(parent, sub, entries, count, docSystem);
|
||||
if (SetIntegerTable(parent, sub, entries.data, entries.count))
|
||||
Api::Documentation::RecordEnum(parent, sub, entries, docSystem);
|
||||
}
|
||||
|
||||
void RegisterConstants(const char *sub, const EnumIntegerEntry *entries, int count,
|
||||
void RegisterConstants(const char *sub, ArrayRef<EnumIntegerEntry> entries,
|
||||
const char *docSystem) {
|
||||
if (SetIntegerTable("Constants", sub, entries, count))
|
||||
Api::Documentation::RecordConstants(sub, entries, count, docSystem);
|
||||
if (SetIntegerTable("Constants", sub, entries.data, entries.count))
|
||||
Api::Documentation::RecordConstants(sub, entries, docSystem);
|
||||
}
|
||||
|
||||
void SetFieldNumber(void *L, const char *key, double value) {
|
||||
|
||||
+59
-29
@@ -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);
|
||||
@@ -137,7 +151,8 @@ int StrCmpI(const char *a, const char *b, int n = 0x7FFFFFFF);
|
||||
// - `type` uses Blizzard's spellings — "number", "string", "bool",
|
||||
// "table", "luaIndex", "UnitToken", "SpellIdentifier", … — or the Name
|
||||
// of a `Structure` / enumeration declared elsewhere, which the browser
|
||||
// renders as a link.
|
||||
// renders as a link. Lua's own spelling is wrong here: "bool", never
|
||||
// "boolean".
|
||||
// - `Function::system` names the System a GLOBAL belongs to; convention
|
||||
// `<Area>Globals` ("SpellGlobals", "ItemGlobals"). Table functions
|
||||
// derive theirs from the table ("C_Spell" → System "Spell", Namespace
|
||||
@@ -147,6 +162,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 +199,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 +220,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 +261,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;
|
||||
@@ -479,6 +502,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
|
||||
@@ -493,14 +518,13 @@ void *ResolveModel(void *L, int idx = 1, bool raiseError = true);
|
||||
// each state records — later passes (every `/reload`) are no-ops — and a
|
||||
// registration made outside a `Run*Registrations` pass is not recorded at
|
||||
// all. The trailing `doc` argument is the descriptor (see `Game::Doc`
|
||||
// above); pass `nullptr` for a not-yet-documented registration and
|
||||
// `_classicapi_UndocumentedAPI()` lists it. The defaults exist only until
|
||||
// the sweep finishes; then they go and a missing descriptor stops compiling.
|
||||
// above). It has no default, so a registration without one does not
|
||||
// compile; only the `_classicapi_` debug functions, which the export skips,
|
||||
// pass `nullptr`.
|
||||
|
||||
// Registers a single global Lua function (e.g. `GetSpellInfo`). The function
|
||||
// must use the WoW Lua C function ABI: `int __fastcall(void *L)`.
|
||||
void RegisterGlobalFunction(const char *name, CFunction func,
|
||||
const Doc::Function *doc = nullptr);
|
||||
void RegisterGlobalFunction(const char *name, CFunction func, const Doc::Function *doc);
|
||||
|
||||
// Glue-state equivalent of `RegisterGlobalFunction`. Identical wire
|
||||
// (calls `FrameScript_RegisterFunction`); the engine routes the
|
||||
@@ -510,8 +534,7 @@ void RegisterGlobalFunction(const char *name, CFunction func,
|
||||
// its own 109 glue functions, and `VAR_LUA_STATE` still points at the
|
||||
// glue state. Calling outside that window would silently target the
|
||||
// wrong state.
|
||||
void RegisterGlueFunction(const char *name, CFunction func,
|
||||
const Doc::Function *doc = nullptr);
|
||||
void RegisterGlueFunction(const char *name, CFunction func, const Doc::Function *doc);
|
||||
|
||||
// Frame-method registration entry: { name, func } pairs walked by the engine's
|
||||
// per-frame-type method-table iterator. Layout matches what the engine expects
|
||||
@@ -523,17 +546,18 @@ 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
|
||||
// are bound — the engine has no built-in support for table-bound Lua
|
||||
// functions, so we manipulate the globals table directly via the Lua C API.
|
||||
void RegisterTableFunction(const char *tableName, const char *methodName,
|
||||
CFunction func, const Doc::Function *doc = nullptr);
|
||||
CFunction func, const Doc::Function *doc);
|
||||
|
||||
// Binds `_G[alias]` to the SAME closure as `_G[tableName][methodName]`, by
|
||||
// value — the way the engine's own Lua-init snippet binds its short aliases
|
||||
@@ -542,7 +566,7 @@ void RegisterTableFunction(const char *tableName, const char *methodName,
|
||||
// captured, so the alias follows the replacement and `alias == table.method`
|
||||
// stays true. Mirrored under `_G.ClassicAPI` like every other registration.
|
||||
void RegisterGlobalAlias(const char *alias, const char *tableName, const char *methodName,
|
||||
const Doc::Function *doc = nullptr);
|
||||
const Doc::Function *doc);
|
||||
|
||||
// Key/value pair for `RegisterIntegerEnum` and `RegisterConstants`. `key`
|
||||
// becomes a field name (PascalCase for Blizzard's `Enum.*`, UPPER_CASE for
|
||||
@@ -557,14 +581,14 @@ 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,
|
||||
const char *docSystem = nullptr);
|
||||
// `entries` is a file-scope array.
|
||||
void RegisterIntegerEnum(const char *parent, const char *sub, ArrayRef<EnumIntegerEntry> entries,
|
||||
const char *docSystem);
|
||||
|
||||
// Registers `Constants[sub] = { entries }`, Blizzard's engine constant
|
||||
// tables (`Constants.MoneyFormattingConstants`). Documented as a "Constants"
|
||||
// table of `docSystem`.
|
||||
void RegisterConstants(const char *sub, const EnumIntegerEntry *entries, int count,
|
||||
void RegisterConstants(const char *sub, ArrayRef<EnumIntegerEntry> entries,
|
||||
const char *docSystem);
|
||||
|
||||
// Set `t[key] = value` on the table currently at stack[-1] (the most
|
||||
@@ -777,6 +801,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
|
||||
|
||||
+15
-2
@@ -164,13 +164,26 @@ int __fastcall Script_HookSecureFunc(void *L) {
|
||||
|
||||
} // namespace
|
||||
|
||||
static const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Opt("table", "table", nullptr,
|
||||
"The table that holds the function; the global table when omitted."),
|
||||
Game::Doc::Req("name", "string", "The key of the function to hook."),
|
||||
Game::Doc::Req("hookfunc", "function",
|
||||
"Called after the original with the same arguments; its returns are "
|
||||
"discarded."),
|
||||
};
|
||||
static const Game::Doc::Function kHookSecureFunc{
|
||||
"Hooks a function so another function runs after it, keeping the original's returns.",
|
||||
kArgs, {}, "FrameScriptGlobals"};
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("hooksecurefunc", &Script_HookSecureFunc);
|
||||
Game::Lua::RegisterGlobalFunction("hooksecurefunc", &Script_HookSecureFunc,
|
||||
&kHookSecureFunc);
|
||||
}
|
||||
// 4.3.4 registers it from FrameScript_Initialize, which runs for the glue
|
||||
// state (Client.cpp) as well as the world (GameUI.cpp).
|
||||
static void RegisterGlueFunctions() {
|
||||
Game::Lua::RegisterGlueFunction("hooksecurefunc", &Script_HookSecureFunc);
|
||||
Game::Lua::RegisterGlueFunction("hooksecurefunc", &Script_HookSecureFunc, &kHookSecureFunc);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
+2545
-73
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -67,7 +67,7 @@ const Game::Doc::Field kIsItemRets[] = {
|
||||
};
|
||||
const Game::Doc::Function kIsItemAction{
|
||||
"Returns 1 if an action slot holds an item.",
|
||||
kSlotArgs, kIsItemRets, "ActionBar"};
|
||||
kSlotArgs, kIsItemRets, "ActionBarGlobals"};
|
||||
|
||||
const Game::Doc::Field kHasBonusRets[] = {
|
||||
Game::Doc::Opt("hasBonusBar", "number", nullptr,
|
||||
@@ -75,7 +75,7 @@ const Game::Doc::Field kHasBonusRets[] = {
|
||||
};
|
||||
const Game::Doc::Function kHasBonusActionBar{
|
||||
"Returns 1 if a bonus action bar is active, for example in a stance or form.",
|
||||
{}, kHasBonusRets, "ActionBar"};
|
||||
{}, kHasBonusRets, "ActionBarGlobals"};
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
+4
-6
@@ -137,21 +137,21 @@ const Game::Doc::Field kFindRets[] = {
|
||||
"holds the spell."),
|
||||
};
|
||||
const Game::Doc::Function kFindSpellActionButtons{
|
||||
"The action slots that hold the spell as a spell action.",
|
||||
"Returns 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.",
|
||||
"Returns 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.",
|
||||
"Returns whether the spell is in an action slot, on the shapeshift bar, or on the pet bar.",
|
||||
kArgs, kOnBarRets};
|
||||
|
||||
const Game::Lua::EnumIntegerEntry kActionBarSet[] = {
|
||||
@@ -170,9 +170,7 @@ static void RegisterLuaFunctions() {
|
||||
&Script_HasSpellActionButtons, &kHasSpellActionButtons);
|
||||
Game::Lua::RegisterTableFunction("C_ActionBar", "IsOnBarOrSpecialBar",
|
||||
&Script_IsOnBarOrSpecialBar, &kIsOnBarOrSpecialBar);
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "ActionBarSet", kActionBarSet,
|
||||
static_cast<int>(sizeof(kActionBarSet) / sizeof(kActionBarSet[0])),
|
||||
"ActionBar");
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "ActionBarSet", kActionBarSet, "ActionBar");
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
+18
-3
@@ -172,7 +172,7 @@ 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 "
|
||||
"Returns 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};
|
||||
|
||||
@@ -180,13 +180,28 @@ 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.",
|
||||
"Returns whether the action is a macro with an explicit #showtooltip line.",
|
||||
kGetSpellArgs, kIsMacroActionWithShowTooltipRets};
|
||||
|
||||
const Game::Doc::Field kGetActionInfoArgs[] = {
|
||||
Game::Doc::Req("slot", "luaIndex", "The action slot, 1 to 120."),
|
||||
};
|
||||
const Game::Doc::Field kGetActionInfoRets[] = {
|
||||
Game::Doc::Opt("actionType", "string", nullptr,
|
||||
"\"spell\", \"macro\", \"item\" or \"equipmentset\"; nothing is returned for "
|
||||
"an empty slot or a macro that no longer exists."),
|
||||
Game::Doc::Opt("id", "number", nullptr,
|
||||
"The spell ID, macro index or item ID; the set name for an equipment set."),
|
||||
Game::Doc::Opt("subType", "string", nullptr, "\"spell\" for a spell, else nil."),
|
||||
};
|
||||
const Game::Doc::Function kGetActionInfo{
|
||||
"Returns what an action slot holds: its type, its ID and a subtype.",
|
||||
kGetActionInfoArgs, kGetActionInfoRets, "ActionBarGlobals"};
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetActionInfo", &Script_GetActionInfo);
|
||||
Game::Lua::RegisterGlobalFunction("GetActionInfo", &Script_GetActionInfo, &kGetActionInfo);
|
||||
Game::Lua::RegisterTableFunction("C_ActionBar", "GetSpell", &Script_GetSpell, &kGetSpellDoc);
|
||||
Game::Lua::RegisterTableFunction("C_ActionBar", "IsMacroActionWithShowTooltip",
|
||||
&Script_IsMacroActionWithShowTooltip,
|
||||
|
||||
@@ -104,10 +104,45 @@ int __fastcall Script_IsConsumableAction(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// --- Documentation ----------------------------------------------------------
|
||||
|
||||
const Game::Doc::Field kSlotArgs[] = {
|
||||
Game::Doc::Req("slot", "luaIndex", "The action slot, 1 to 120."),
|
||||
};
|
||||
|
||||
const Game::Doc::Field kCooldownRets[] = {
|
||||
Game::Doc::Req("startTime", "number", "In GetTime() seconds; 0 when there is no cooldown."),
|
||||
Game::Doc::Req("duration", "number", "In seconds; 0 when there is no cooldown."),
|
||||
Game::Doc::Req("enable", "number",
|
||||
"0 while the cooldown waits to start, such as for a potion used in combat."),
|
||||
};
|
||||
const Game::Doc::Function kGetActionCooldown{
|
||||
"Returns the cooldown of an action, also for a macro that shows an item.",
|
||||
kSlotArgs, kCooldownRets, "ActionBarGlobals"};
|
||||
|
||||
const Game::Doc::Field kCountRets[] = {
|
||||
Game::Doc::Req("count", "number"),
|
||||
};
|
||||
const Game::Doc::Function kGetActionCount{
|
||||
"Returns how many of an action's item or reagent you carry, also for a macro that shows "
|
||||
"an item.",
|
||||
kSlotArgs, kCountRets, "ActionBarGlobals"};
|
||||
|
||||
const Game::Doc::Field kConsumableRets[] = {
|
||||
Game::Doc::Opt("isConsumable", "number", nullptr,
|
||||
"1 when the action uses up an item, else nil."),
|
||||
};
|
||||
const Game::Doc::Function kIsConsumableAction{
|
||||
"Returns whether an action uses up an item, also for a macro that shows an item.",
|
||||
kSlotArgs, kConsumableRets, "ActionBarGlobals"};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetActionCooldown", &Script_GetActionCooldown);
|
||||
Game::Lua::RegisterGlobalFunction("GetActionCount", &Script_GetActionCount);
|
||||
Game::Lua::RegisterGlobalFunction("IsConsumableAction", &Script_IsConsumableAction);
|
||||
Game::Lua::RegisterGlobalFunction("GetActionCooldown", &Script_GetActionCooldown,
|
||||
&kGetActionCooldown);
|
||||
Game::Lua::RegisterGlobalFunction("GetActionCount", &Script_GetActionCount,
|
||||
&kGetActionCount);
|
||||
Game::Lua::RegisterGlobalFunction("IsConsumableAction", &Script_IsConsumableAction,
|
||||
&kIsConsumableAction);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -126,8 +126,8 @@ const Game::Doc::Field kEnableArgs[] = {
|
||||
"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.",
|
||||
"Turns ACTION_RANGE_CHECK_UPDATE on or off for an action slot, which fires when the action "
|
||||
"goes in or out of range of its target.",
|
||||
kEnableArgs};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
|
||||
@@ -111,7 +111,7 @@ const Game::Doc::Field kIsStackableRets[] = {
|
||||
};
|
||||
const Game::Doc::Function kIsStackableAction{
|
||||
"Returns 1 if an action slot holds an item that stacks.",
|
||||
kSlotArgs, kIsStackableRets, "ActionBar"};
|
||||
kSlotArgs, kIsStackableRets, "ActionBarGlobals"};
|
||||
|
||||
const Game::Doc::Field kActionIDArgs[] = {
|
||||
Game::Doc::Req("actionID", "luaIndex"),
|
||||
@@ -120,7 +120,7 @@ const Game::Doc::Field kCIsStackableRets[] = {
|
||||
Game::Doc::Req("isStackableAction", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kCIsStackableAction{
|
||||
"Whether the action's item stacks.",
|
||||
"Returns whether the action's item stacks.",
|
||||
kActionIDArgs, kCIsStackableRets};
|
||||
|
||||
const Game::Doc::Field kOnEquipRets[] = {
|
||||
@@ -128,7 +128,7 @@ const Game::Doc::Field kOnEquipRets[] = {
|
||||
"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.",
|
||||
"Returns the spell the action's item grants while equipped.",
|
||||
kActionIDArgs, kOnEquipRets};
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
#include "addons/TocRewrite.h"
|
||||
#include "bindings/Inject.h"
|
||||
#include "embed/Resource.h"
|
||||
#include "storm/Memory.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
@@ -153,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).
|
||||
@@ -277,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.
|
||||
@@ -320,10 +318,8 @@ int ReadAddonFile(int addon, const char *relPath, int unused, const char *path,
|
||||
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);
|
||||
void *buf = Storm::Alloc(totalSize, __FILE__, __LINE__);
|
||||
if (buf == nullptr) return 0;
|
||||
std::memcpy(buf, entry.data, entry.size);
|
||||
if (extraBytes > 0) {
|
||||
|
||||
@@ -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
|
||||
|
||||
+88
-14
@@ -368,6 +368,81 @@ constexpr Game::Lua::EnumIntegerEntry kAddOnSecurityStatusEntries[] = {
|
||||
{"NotAvailable", 3},
|
||||
};
|
||||
|
||||
const Game::Doc::Field kNameArg[] = {
|
||||
Game::Doc::Req("name", "uiAddon", "An addon index or folder name."),
|
||||
};
|
||||
|
||||
const Game::Doc::Field kGetNameRets[] = {
|
||||
Game::Doc::Opt("name", "cstring", nullptr,
|
||||
"The folder name; a name argument is returned as given. Nil for an unknown "
|
||||
"addon."),
|
||||
};
|
||||
const Game::Doc::Function kGetAddOnName{"Returns the folder name of an addon.", kNameArg,
|
||||
kGetNameRets};
|
||||
|
||||
const Game::Doc::Field kGetTitleRets[] = {
|
||||
Game::Doc::Opt("title", "cstring", nullptr,
|
||||
"Nil for an unknown addon or one without a title."),
|
||||
};
|
||||
const Game::Doc::Function kGetAddOnTitle{"Returns the ## Title: of an addon's TOC file.",
|
||||
kNameArg, kGetTitleRets};
|
||||
|
||||
const Game::Doc::Field kGetNotesRets[] = {
|
||||
Game::Doc::Opt("notes", "cstring", nullptr,
|
||||
"Nil for an unknown addon or one without notes."),
|
||||
};
|
||||
const Game::Doc::Function kGetAddOnNotes{"Returns the ## Notes: of an addon's TOC file.",
|
||||
kNameArg, kGetNotesRets};
|
||||
|
||||
const Game::Doc::Field kLoadableArgs[] = {
|
||||
Game::Doc::Req("name", "uiAddon", "An addon index or folder name."),
|
||||
Game::Doc::Opt("character", "cstring", nullptr, "Ignored."),
|
||||
Game::Doc::Opt("demandLoaded", "bool", nullptr, "Ignored."),
|
||||
};
|
||||
const Game::Doc::Field kLoadableRets[] = {
|
||||
Game::Doc::Req("loadable", "bool", "True for an addon that is already loaded."),
|
||||
Game::Doc::Opt("reason", "cstring", nullptr,
|
||||
"Why it cannot load, such as \"DISABLED\" or \"INTERFACE_VERSION\"; nil "
|
||||
"when it can, or when no addon has that name."),
|
||||
};
|
||||
const Game::Doc::Function kIsAddOnLoadable{"Returns whether an addon can be loaded, and why "
|
||||
"not when it cannot.",
|
||||
kLoadableArgs, kLoadableRets};
|
||||
|
||||
const Game::Doc::Field kSecurityRets[] = {
|
||||
Game::Doc::Req("security", "AddOnSecurityStatus", "NotAvailable for an unknown addon."),
|
||||
};
|
||||
const Game::Doc::Function kGetAddOnSecurity{"Returns the security status of an addon.",
|
||||
kNameArg, kSecurityRets};
|
||||
|
||||
const Game::Doc::Field kExistsRets[] = {
|
||||
Game::Doc::Req("exists", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kDoesAddOnExist{"Returns whether an addon is installed.", kNameArg,
|
||||
kExistsRets};
|
||||
|
||||
const Game::Doc::Field kLoadedRets[] = {
|
||||
Game::Doc::Req("loadedOrLoading", "bool"),
|
||||
Game::Doc::Req("loaded", "bool", "False while the addon's own files are still running."),
|
||||
};
|
||||
const Game::Doc::Function kIsAddOnLoaded{"Returns whether an addon has started loading and "
|
||||
"whether it has finished.",
|
||||
kNameArg, kLoadedRets};
|
||||
|
||||
const Game::Doc::Field kLoadRets[] = {
|
||||
Game::Doc::Opt("loaded", "number", nullptr,
|
||||
"1 when the addon loaded or was already loaded; nil otherwise."),
|
||||
Game::Doc::Opt("value", "string", nullptr,
|
||||
"Why the load failed, such as \"DISABLED\"; nil on success."),
|
||||
};
|
||||
const Game::Doc::Function kLoadAddOn{"Loads a load-on-demand addon.", kNameArg, kLoadRets};
|
||||
|
||||
const Game::Doc::Field kOptionalDepsRets[] = {
|
||||
Game::Doc::Vararg("cstring", "The ## OptionalDeps: names, in order."),
|
||||
};
|
||||
const Game::Doc::Function kGetAddOnOptionalDependencies{
|
||||
"Returns the optional dependencies of an addon.", kNameArg, kOptionalDepsRets};
|
||||
|
||||
const Game::Doc::Field kInterfaceVersionArgs[] = {
|
||||
Game::Doc::Req("name", "uiAddon", "An addon index or folder name."),
|
||||
};
|
||||
@@ -384,35 +459,34 @@ const Game::Doc::Function kGetAddOnInterfaceVersion{
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_AddOns", "GetAddOnName",
|
||||
&Script_GetAddOnName);
|
||||
&Script_GetAddOnName, &kGetAddOnName);
|
||||
Game::Lua::RegisterTableFunction("C_AddOns", "GetAddOnTitle",
|
||||
&Script_GetAddOnTitle);
|
||||
&Script_GetAddOnTitle, &kGetAddOnTitle);
|
||||
Game::Lua::RegisterTableFunction("C_AddOns", "GetAddOnNotes",
|
||||
&Script_GetAddOnNotes);
|
||||
&Script_GetAddOnNotes, &kGetAddOnNotes);
|
||||
Game::Lua::RegisterTableFunction("C_AddOns", "IsAddOnLoadable",
|
||||
&Script_IsAddOnLoadable);
|
||||
&Script_IsAddOnLoadable, &kIsAddOnLoadable);
|
||||
Game::Lua::RegisterTableFunction("C_AddOns", "GetAddOnSecurity",
|
||||
&Script_GetAddOnSecurity);
|
||||
&Script_GetAddOnSecurity, &kGetAddOnSecurity);
|
||||
Game::Lua::RegisterTableFunction("C_AddOns", "DoesAddOnExist",
|
||||
&Script_DoesAddOnExist);
|
||||
&Script_DoesAddOnExist, &kDoesAddOnExist);
|
||||
Game::Lua::RegisterTableFunction("C_AddOns", "IsAddOnLoaded",
|
||||
&Script_IsAddOnLoaded);
|
||||
&Script_IsAddOnLoaded, &kIsAddOnLoaded);
|
||||
// The stock global, mirrored into the namespace — the engine's own
|
||||
// handler already has the Lua C ABI and the modern return shape.
|
||||
Game::Lua::RegisterTableFunction(
|
||||
"C_AddOns", "LoadAddOn",
|
||||
reinterpret_cast<Game::Lua::CFunction>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_LOAD_ADDON)));
|
||||
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_LOAD_ADDON)),
|
||||
&kLoadAddOn);
|
||||
Game::Lua::RegisterTableFunction("C_AddOns", "GetAddOnOptionalDependencies",
|
||||
&Script_GetAddOnOptionalDependencies);
|
||||
&Script_GetAddOnOptionalDependencies,
|
||||
&kGetAddOnOptionalDependencies);
|
||||
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,
|
||||
"AddOns");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -120,9 +119,20 @@ int __fastcall Script_GetAddOnLocalTable(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("name", "uiAddon", "The addon's folder name; an index is not accepted."),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Opt("table", "table", nullptr,
|
||||
"The table the addon's files receive as their second argument; nil unless "
|
||||
"the addon is loaded and its TOC sets ## AllowAddOnTableAccess: 1."),
|
||||
};
|
||||
const Game::Doc::Function kGetAddOnLocalTable{
|
||||
"Returns the private table of another addon that allows access to it.", kArgs, kRets};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_AddOns", "GetAddOnLocalTable",
|
||||
&Script_GetAddOnLocalTable);
|
||||
&Script_GetAddOnLocalTable, &kGetAddOnLocalTable);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -175,8 +175,8 @@ static void RegisterLuaFunctions() {
|
||||
&kUpdateAddOnMemoryUsage);
|
||||
Game::Lua::RegisterGlobalFunction("GetAddOnMemoryUsage", &Script_GetAddOnMemoryUsage,
|
||||
&kGetAddOnMemoryUsage);
|
||||
Game::Lua::RegisterGlobalFunction("_classicapi_LuaHeapAudit", &Script_LuaHeapAudit);
|
||||
Game::Lua::RegisterGlobalFunction("_classicapi_GcTagOf", &Script_GcTagOf);
|
||||
Game::Lua::RegisterGlobalFunction("_classicapi_LuaHeapAudit", &Script_LuaHeapAudit, nullptr);
|
||||
Game::Lua::RegisterGlobalFunction("_classicapi_GcTagOf", &Script_GcTagOf, nullptr);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,10 @@
|
||||
#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>
|
||||
@@ -27,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) {
|
||||
@@ -66,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;
|
||||
}
|
||||
|
||||
@@ -158,7 +148,7 @@ bool EvalCondition(const char *head, size_t headLen, const char *args,
|
||||
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;
|
||||
@@ -297,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;
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "frame/Layout.h"
|
||||
#include "frame/Alpha.h"
|
||||
#include "frame/Destruction.h"
|
||||
#include "texture/Transform.h"
|
||||
@@ -115,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);
|
||||
|
||||
@@ -216,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;
|
||||
}
|
||||
@@ -560,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};
|
||||
|
||||
@@ -59,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')
|
||||
@@ -145,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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -162,13 +162,22 @@ struct WidgetSystem {
|
||||
};
|
||||
const WidgetSystem kWidgetSystems[] = {
|
||||
{Offsets::VAR_GAMETOOLTIP_METHOD_REGISTRY, "GameTooltipAPI"},
|
||||
{Offsets::VAR_REGION_METHOD_REGISTRY, "ScriptRegionAPI"},
|
||||
{Offsets::VAR_REGION_METHOD_REGISTRY, "SimpleScriptRegionAPI"},
|
||||
{Offsets::VAR_FRAME_METHOD_REGISTRY, "SimpleFrameAPI"},
|
||||
{Offsets::VAR_TEXTURE_METHOD_REGISTRY, "SimpleTextureAPI"},
|
||||
{Offsets::VAR_FONTSTRING_METHOD_REGISTRY, "SimpleFontStringAPI"},
|
||||
{Offsets::VAR_FONT_METHOD_REGISTRY, "SimpleFontAPI"},
|
||||
{Offsets::VAR_EDITBOX_METHOD_REGISTRY, "SimpleEditBoxAPI"},
|
||||
{Offsets::VAR_MODEL_METHOD_REGISTRY, "SimpleModelAPI"},
|
||||
{Offsets::VAR_BUTTON_METHOD_REGISTRY, "SimpleButtonAPI"},
|
||||
{Offsets::VAR_SLIDER_METHOD_REGISTRY, "SimpleSliderAPI"},
|
||||
{Offsets::VAR_STATUSBAR_METHOD_REGISTRY, "SimpleStatusBarAPI"},
|
||||
{Offsets::VAR_MESSAGEFRAME_METHOD_REGISTRY, "SimpleMessageFrameAPI"},
|
||||
{Offsets::VAR_SIMPLEHTML_METHOD_REGISTRY, "SimpleHTMLAPI"},
|
||||
// These three have no retail counterpart; they are named the same way.
|
||||
{Offsets::VAR_SCROLLINGMESSAGEFRAME_METHOD_REGISTRY, "SimpleScrollingMessageFrameAPI"},
|
||||
{Offsets::VAR_PLAYERMODEL_METHOD_REGISTRY, "SimplePlayerModelAPI"},
|
||||
{Offsets::VAR_DRESSUPMODEL_METHOD_REGISTRY, "SimpleDressUpModelAPI"},
|
||||
};
|
||||
|
||||
// Hashes of the frame classes this DLL adds (NameWidgetSystem).
|
||||
@@ -637,7 +646,8 @@ void RegisterLuaFunctions() {
|
||||
&kGetSystems);
|
||||
Game::Lua::RegisterTableFunction("C_APIDocumentation", "GetSystem", &Script_GetSystem,
|
||||
&kGetSystem);
|
||||
Game::Lua::RegisterGlobalFunction("_classicapi_UndocumentedAPI", &Script_UndocumentedAPI);
|
||||
Game::Lua::RegisterGlobalFunction("_classicapi_UndocumentedAPI", &Script_UndocumentedAPI,
|
||||
nullptr);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
@@ -665,10 +675,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) {
|
||||
@@ -720,14 +735,14 @@ void RecordValueTable(Kind kind, const char *parent, const char *sub,
|
||||
|
||||
} // namespace
|
||||
|
||||
void RecordEnum(const char *parent, const char *sub, const Game::Lua::EnumIntegerEntry *entries,
|
||||
int count, const char *system) {
|
||||
RecordValueTable(KIND_ENUM, parent, sub, entries, count, system);
|
||||
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, const Game::Lua::EnumIntegerEntry *entries, int count,
|
||||
void RecordConstants(const char *sub, Game::ArrayRef<Game::Lua::EnumIntegerEntry> entries,
|
||||
const char *system) {
|
||||
RecordValueTable(KIND_CONSTANTS, "Constants", sub, entries, count, system);
|
||||
RecordValueTable(KIND_CONSTANTS, "Constants", sub, entries.data, entries.count, system);
|
||||
}
|
||||
|
||||
} // namespace Api::Documentation
|
||||
|
||||
@@ -32,14 +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 RecordConstants(const char *sub, const Game::Lua::EnumIntegerEntry *entries, int count,
|
||||
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
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
// buyout normalization, runTime table check, auctioneer/"AH open" check.
|
||||
//
|
||||
// Events (retail multi-sell names, fired via Event::Custom):
|
||||
// AUCTION_MULTISELL_START — job accepted, posting begins
|
||||
// AUCTION_MULTISELL_START(total) — job accepted, posting begins
|
||||
// AUCTION_MULTISELL_UPDATE(done, total) — after each stack lands
|
||||
// AUCTION_MULTISELL_FAILURE — aborted (source changed, no slot,
|
||||
// AH closed, or a step timed out)
|
||||
@@ -68,9 +68,26 @@ namespace AuctionHouse::PostItem {
|
||||
|
||||
namespace {
|
||||
|
||||
const Event::Custom::AutoReserve _evtStart{"AUCTION_MULTISELL_START"};
|
||||
const Event::Custom::AutoReserve _evtUpdate{"AUCTION_MULTISELL_UPDATE"};
|
||||
const Event::Custom::AutoReserve _evtFailure{"AUCTION_MULTISELL_FAILURE"};
|
||||
const Game::Doc::Field kStartPayload[] = {
|
||||
Game::Doc::Req("numRepetitions", "number", "The auctions the job will post."),
|
||||
};
|
||||
const Game::Doc::Event kStartDoc{
|
||||
"AuctionHouse", "Fires when C_AuctionHouse.PostItem accepts a job and starts to post.",
|
||||
kStartPayload};
|
||||
const Game::Doc::Field kUpdatePayload[] = {
|
||||
Game::Doc::Req("createdCount", "number", "The auctions posted so far."),
|
||||
Game::Doc::Req("totalToCreate", "number"),
|
||||
};
|
||||
const Game::Doc::Event kUpdateDoc{
|
||||
"AuctionHouse", "Fires after each auction that C_AuctionHouse.PostItem posts.",
|
||||
kUpdatePayload};
|
||||
const Game::Doc::Event kFailureDoc{
|
||||
"AuctionHouse", "Fires when a C_AuctionHouse.PostItem job stops before it posts all auctions.",
|
||||
{}};
|
||||
|
||||
const Event::Custom::AutoReserve _evtStart{"AUCTION_MULTISELL_START", &kStartDoc};
|
||||
const Event::Custom::AutoReserve _evtUpdate{"AUCTION_MULTISELL_UPDATE", &kUpdateDoc};
|
||||
const Event::Custom::AutoReserve _evtFailure{"AUCTION_MULTISELL_FAILURE", &kFailureDoc};
|
||||
|
||||
// Generous per-step abort bound. A split or a post is one server round-trip
|
||||
// (~tens of frames); 600 frames (~10s at 60fps) means "something's wrong"
|
||||
@@ -397,14 +414,31 @@ int __fastcall Script_C_AuctionHouse_PostItem(void *L) {
|
||||
g_job.runTime = runTime;
|
||||
g_job.phase = Phase::Idle;
|
||||
|
||||
Event::Custom::Fire(_evtStart.Slot(), "");
|
||||
Event::Custom::Fire(_evtStart.Slot(), "%d", numStacks);
|
||||
Game::Lua::PushBool(L, true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kPostItemArgs[] = {
|
||||
Game::Doc::Req("itemLocation", "ItemLocation", "A bag slot: {bagID = B, slotIndex = S}."),
|
||||
Game::Doc::Req("duration", "number",
|
||||
"1, 2 or 3 for 2, 8 or 24 hours; 120, 480 or 1440 also work."),
|
||||
Game::Doc::Req("quantity", "number", "Items in each auction, 1 to 255."),
|
||||
Game::Doc::Req("numStacks", "number", "The number of auctions."),
|
||||
Game::Doc::Req("bid", "number", "In copper, for each auction."),
|
||||
Game::Doc::Opt("buyout", "number", "0", "In copper, for each auction; 0 for no buyout."),
|
||||
};
|
||||
const Game::Doc::Field kPostItemRets[] = {
|
||||
Game::Doc::Req("accepted", "bool"),
|
||||
Game::Doc::Opt("reason", "string", nullptr, "Why the job was refused; nil when accepted."),
|
||||
};
|
||||
const Game::Doc::Function kPostItem{
|
||||
"Starts to post several auctions of one size from one bag slot.", kPostItemArgs,
|
||||
kPostItemRets};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_AuctionHouse", "PostItem",
|
||||
&Script_C_AuctionHouse_PostItem);
|
||||
&Script_C_AuctionHouse_PostItem, &kPostItem);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
+191
-15
@@ -514,7 +514,7 @@ const Game::Doc::Field kByInstanceIDRets[] = {
|
||||
Game::Doc::Opt("aura", "AuraData", nullptr, "Nil when no aura on the unit has that ID."),
|
||||
};
|
||||
const Game::Doc::Function kGetAuraDataByAuraInstanceID{
|
||||
"The aura with the given instance ID on a unit.", kByInstanceIDArgs, kByInstanceIDRets};
|
||||
"Returns the aura with the given instance ID on a unit.", kByInstanceIDArgs, kByInstanceIDRets};
|
||||
|
||||
// C_UnitAuras.GetAuraDataBySlot(unit, slot) -> AuraData | nil
|
||||
int __fastcall Script_GetAuraDataBySlot(void *L) {
|
||||
@@ -702,40 +702,216 @@ 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);
|
||||
&Script_GetAuraDataByIndex, &kGetAuraDataByIndex);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetBuffDataByIndex",
|
||||
&Script_GetBuffDataByIndex);
|
||||
&Script_GetBuffDataByIndex, &kGetBuffDataByIndex);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetDebuffDataByIndex",
|
||||
&Script_GetDebuffDataByIndex);
|
||||
&Script_GetDebuffDataByIndex, &kGetDebuffDataByIndex);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitAura",
|
||||
&Script_UnitAura);
|
||||
&Script_UnitAura, &kUnitAura);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitBuff",
|
||||
&Script_UnitBuff);
|
||||
&Script_UnitBuff, &kUnitBuff);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitDebuff",
|
||||
&Script_UnitDebuff);
|
||||
&Script_UnitDebuff, &kUnitDebuff);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraSlots",
|
||||
&Script_GetAuraSlots);
|
||||
&Script_GetAuraSlots, &kGetAuraSlots);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataBySlot",
|
||||
&Script_GetAuraDataBySlot);
|
||||
&Script_GetAuraDataBySlot, &kGetAuraDataBySlot);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataByAuraInstanceID",
|
||||
&Script_GetAuraDataByAuraInstanceID,
|
||||
&kGetAuraDataByAuraInstanceID);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitAuraBySlot",
|
||||
&Script_UnitAuraBySlot);
|
||||
&Script_UnitAuraBySlot, &kUnitAuraBySlot);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetUnitAuraBySpellID",
|
||||
&Script_GetUnitAuraBySpellID);
|
||||
&Script_GetUnitAuraBySpellID, &kGetUnitAuraBySpellID);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetPlayerAuraBySpellID",
|
||||
&Script_GetPlayerAuraBySpellID);
|
||||
&Script_GetPlayerAuraBySpellID, &kGetPlayerAuraBySpellID);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataBySpellName",
|
||||
&Script_GetAuraDataBySpellName);
|
||||
&Script_GetAuraDataBySpellName, &kGetAuraDataBySpellName);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetUnitAuras",
|
||||
&Script_GetUnitAuras);
|
||||
&Script_GetUnitAuras, &kGetUnitAuras);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDispelTypeColor",
|
||||
&Script_GetAuraDispelTypeColor);
|
||||
&Script_GetAuraDispelTypeColor, &kGetAuraDispelTypeColor);
|
||||
Game::Lua::RegisterGlobalFunction("CancelUnitBuff", &Script_CancelUnitBuff,
|
||||
&kCancelUnitBuff);
|
||||
}
|
||||
|
||||
@@ -193,9 +193,23 @@ 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);
|
||||
&Script_RegisterComboDuration, &kRegisterComboDuration);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
+24
-10
@@ -946,10 +946,31 @@ 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);
|
||||
&Script_RegisterAuraDurationModifierByTrigger,
|
||||
&kRegisterAuraDurationModifierByTrigger);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoregDurationMod{&RegisterDurationModLua};
|
||||
@@ -1023,16 +1044,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
|
||||
|
||||
@@ -380,7 +380,7 @@ 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};
|
||||
"Returns the known players whose names complete the typed text.", kResultsArgs, kResultsRets};
|
||||
|
||||
const Game::Doc::Field kRecognizedArgs[] = {
|
||||
Game::Doc::Req("name", "string"),
|
||||
@@ -392,7 +392,7 @@ const Game::Doc::Field kRecognizedArgs[] = {
|
||||
const Game::Doc::Field kRecognizedRets[] = {
|
||||
Game::Doc::Req("isRecognizedName", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kIsRecognizedName{"Whether a known player has exactly this name.",
|
||||
const Game::Doc::Function kIsRecognizedName{"Returns whether a known player has exactly this name.",
|
||||
kRecognizedArgs, kRecognizedRets};
|
||||
|
||||
const Game::Lua::EnumIntegerEntry kEntryFlagEntries[] = {
|
||||
@@ -457,12 +457,10 @@ static void RegisterLuaFunctions() {
|
||||
&Script_GetAutoCompleteResults, &kGetAutoCompleteResults);
|
||||
Game::Lua::RegisterTableFunction("C_AutoComplete", "IsRecognizedName", &Script_IsRecognizedName,
|
||||
&kIsRecognizedName);
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "AutoCompleteEntryFlag", kEntryFlagEntries,
|
||||
sizeof(kEntryFlagEntries) / sizeof(kEntryFlagEntries[0]),
|
||||
"AutoComplete");
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "AutoCompletePriority", kPriorityEntries,
|
||||
sizeof(kPriorityEntries) / sizeof(kPriorityEntries[0]),
|
||||
"AutoComplete");
|
||||
Game::Lua::RegisterIntegerEnum(
|
||||
"Enum", "AutoCompleteEntryFlag", kEntryFlagEntries, "AutoComplete");
|
||||
Game::Lua::RegisterIntegerEnum(
|
||||
"Enum", "AutoCompletePriority", kPriorityEntries, "AutoComplete");
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -119,6 +119,23 @@ bool Stash(void *L, const char *name, const char *key) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kCollectgarbageArgs[] = {
|
||||
Game::Doc::Opt("opt", "string", nullptr,
|
||||
"\"collect\", \"count\", \"step\", \"stop\", \"restart\", \"setpause\" or "
|
||||
"\"setstepmul\"; a number or nothing sets the KB threshold for the next "
|
||||
"collection, 0 collects now."),
|
||||
Game::Doc::Opt("arg", "number", nullptr, "Has no effect."),
|
||||
};
|
||||
const Game::Doc::Field kCollectgarbageRets[] = {
|
||||
Game::Doc::Opt("result", "any", nullptr,
|
||||
"The KB of Lua memory in use for \"count\", true for \"step\", 0 for the other "
|
||||
"options, nothing for a threshold."),
|
||||
};
|
||||
const Game::Doc::Function kCollectgarbage{
|
||||
"Runs a garbage collection or returns the memory in use; the other options are accepted "
|
||||
"and do nothing.",
|
||||
kCollectgarbageArgs, kCollectgarbageRets, "LuaGlobals"};
|
||||
|
||||
void DoRegister(bool glue) {
|
||||
void *L = Game::Lua::State();
|
||||
if (L == nullptr)
|
||||
@@ -129,9 +146,11 @@ void DoRegister(bool glue) {
|
||||
return;
|
||||
Stash(L, "gcinfo", kOrigGcInfo); // optional; "count" degrades to 0
|
||||
if (glue)
|
||||
Game::Lua::RegisterGlueFunction("collectgarbage", &Script_collectgarbage);
|
||||
Game::Lua::RegisterGlueFunction("collectgarbage", &Script_collectgarbage,
|
||||
&kCollectgarbage);
|
||||
else
|
||||
Game::Lua::RegisterGlobalFunction("collectgarbage", &Script_collectgarbage);
|
||||
Game::Lua::RegisterGlobalFunction("collectgarbage", &Script_collectgarbage,
|
||||
&kCollectgarbage);
|
||||
}
|
||||
|
||||
void RegisterInGame() { DoRegister(false); }
|
||||
|
||||
@@ -193,7 +193,7 @@ int __fastcall Script_debuglocals(void *L) {
|
||||
|
||||
const Game::Doc::Field kDebuglocalsArgs[] = {
|
||||
Game::Doc::Opt("level", "number", "1", "The call level, as debugstack counts it."),
|
||||
Game::Doc::Opt("skipFunctionsAndUserdata", "boolean", "false",
|
||||
Game::Doc::Opt("skipFunctionsAndUserdata", "bool", "false",
|
||||
"Leaves out function and userdata values."),
|
||||
};
|
||||
const Game::Doc::Field kDebuglocalsRets[] = {
|
||||
@@ -202,7 +202,7 @@ const Game::Doc::Field kDebuglocalsRets[] = {
|
||||
"error handler."),
|
||||
};
|
||||
const Game::Doc::Function kDebuglocals{
|
||||
"The local variables and upvalues of a call level as text, for an error report.",
|
||||
"Returns the local variables and upvalues of a call level as text, for an error report.",
|
||||
kDebuglocalsArgs, kDebuglocalsRets, "LuaGlobals"};
|
||||
|
||||
void RegisterInGame() {
|
||||
|
||||
@@ -84,14 +84,29 @@ void Apply() {
|
||||
reinterpret_cast<const void *>(&GetInfo));
|
||||
}
|
||||
|
||||
const Game::Doc::Field kDebugstackArgs[] = {
|
||||
Game::Doc::Opt("thread", "thread", nullptr,
|
||||
"A coroutine whose stack is shown instead of the caller's."),
|
||||
Game::Doc::Opt("start", "number", nullptr,
|
||||
"The first call level to show; 1, the caller, by default, or 0, the "
|
||||
"innermost level, with a thread."),
|
||||
Game::Doc::Opt("count1", "number", "12", "The number of levels to show from the top."),
|
||||
Game::Doc::Opt("count2", "number", "10", "The number of levels to show from the bottom."),
|
||||
};
|
||||
const Game::Doc::Field kDebugstackRets[] = {
|
||||
Game::Doc::Req("stack", "string", "One line for each call level."),
|
||||
};
|
||||
const Game::Doc::Function kDebugstack{"Returns the call stack as text, for an error report.",
|
||||
kDebugstackArgs, kDebugstackRets, "LuaGlobals"};
|
||||
|
||||
void RegisterInGame() {
|
||||
Apply();
|
||||
Lua::RegisterGlobalFunction("debugstack", &Script_debugstack);
|
||||
Lua::RegisterGlobalFunction("debugstack", &Script_debugstack, &kDebugstack);
|
||||
}
|
||||
|
||||
void RegisterGlue() {
|
||||
Apply();
|
||||
Lua::RegisterGlueFunction("debugstack", &Script_debugstack);
|
||||
Lua::RegisterGlueFunction("debugstack", &Script_debugstack, &kDebugstack);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterInGame};
|
||||
|
||||
@@ -39,7 +39,7 @@ const Game::Doc::Field kDifftimeArgs[] = {
|
||||
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.",
|
||||
const Game::Doc::Function kDifftime{"Returns the number of seconds from time1 to time2.",
|
||||
kDifftimeArgs, kDifftimeRets, "LuaGlobals"};
|
||||
|
||||
void RegisterInGame() {
|
||||
|
||||
+33
-5
@@ -71,12 +71,40 @@ int __fastcall Script_math_tanh(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kFmodArgs[] = {
|
||||
Game::Doc::Req("x", "number"),
|
||||
Game::Doc::Req("y", "number"),
|
||||
};
|
||||
const Game::Doc::Field kFmodRets[] = {
|
||||
Game::Doc::Req("remainder", "number", "Has the sign of x."),
|
||||
};
|
||||
const Game::Doc::Function kFmod{
|
||||
"Returns the remainder of x divided by y, with the quotient rounded toward zero.",
|
||||
kFmodArgs, kFmodRets};
|
||||
|
||||
const Game::Doc::Field kXArgs[] = {
|
||||
Game::Doc::Req("x", "number"),
|
||||
};
|
||||
const Game::Doc::Field kModfRets[] = {
|
||||
Game::Doc::Req("integral", "number", "The whole-number part, rounded toward zero."),
|
||||
Game::Doc::Req("fractional", "number", "The rest of x, with the sign of x."),
|
||||
};
|
||||
const Game::Doc::Function kModf{"Splits a number into its integral and fractional parts.",
|
||||
kXArgs, kModfRets};
|
||||
|
||||
const Game::Doc::Field kResultRets[] = {
|
||||
Game::Doc::Req("result", "number"),
|
||||
};
|
||||
const Game::Doc::Function kCosh{"Returns the hyperbolic cosine of x.", kXArgs, kResultRets};
|
||||
const Game::Doc::Function kSinh{"Returns the hyperbolic sine of x.", kXArgs, kResultRets};
|
||||
const Game::Doc::Function kTanh{"Returns the hyperbolic tangent of x.", kXArgs, kResultRets};
|
||||
|
||||
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);
|
||||
Game::Lua::RegisterTableFunction("math", "fmod", Script_math_fmod, &kFmod);
|
||||
Game::Lua::RegisterTableFunction("math", "modf", &Script_math_modf, &kModf);
|
||||
Game::Lua::RegisterTableFunction("math", "cosh", &Script_math_cosh, &kCosh);
|
||||
Game::Lua::RegisterTableFunction("math", "sinh", &Script_math_sinh, &kSinh);
|
||||
Game::Lua::RegisterTableFunction("math", "tanh", &Script_math_tanh, &kTanh);
|
||||
|
||||
// `math.huge` — a numeric constant, not a function. Fetch the (now
|
||||
// guaranteed-present) `math` table and set the field directly.
|
||||
|
||||
+25
-2
@@ -70,9 +70,32 @@ int __fastcall Script_CreateFromMixins(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kMixinArgs[] = {
|
||||
Game::Doc::Req("object", "table", "The table that receives the fields."),
|
||||
Game::Doc::Vararg("table", "The mixins; a later one wins when two have the same key, and a "
|
||||
"value that is not a table is skipped."),
|
||||
};
|
||||
const Game::Doc::Field kMixinRets[] = {
|
||||
Game::Doc::Req("object", "table", "The same table as the object argument."),
|
||||
};
|
||||
const Game::Doc::Function kMixin{"Copies every field of each mixin onto object.",
|
||||
kMixinArgs, kMixinRets, "FrameScriptGlobals"};
|
||||
|
||||
const Game::Doc::Field kCreateFromMixinsArgs[] = {
|
||||
Game::Doc::Vararg("table", "The mixins; a later one wins when two have the same key, and a "
|
||||
"value that is not a table is skipped."),
|
||||
};
|
||||
const Game::Doc::Field kCreateFromMixinsRets[] = {
|
||||
Game::Doc::Req("object", "table"),
|
||||
};
|
||||
const Game::Doc::Function kCreateFromMixins{
|
||||
"Returns a new table with every field of each mixin copied onto it.",
|
||||
kCreateFromMixinsArgs, kCreateFromMixinsRets, "FrameScriptGlobals"};
|
||||
|
||||
void RegisterFns() {
|
||||
Game::Lua::RegisterGlobalFunction("Mixin", &Script_Mixin);
|
||||
Game::Lua::RegisterGlobalFunction("CreateFromMixins", &Script_CreateFromMixins);
|
||||
Game::Lua::RegisterGlobalFunction("Mixin", &Script_Mixin, &kMixin);
|
||||
Game::Lua::RegisterGlobalFunction("CreateFromMixins", &Script_CreateFromMixins,
|
||||
&kCreateFromMixins);
|
||||
}
|
||||
|
||||
// Pure table helpers — register on both the in-game and glue states so they're
|
||||
|
||||
+59
-34
@@ -17,8 +17,11 @@
|
||||
// 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".
|
||||
// As in 5.1, a weak table marks the valid metatables (weak[m] = true) and is
|
||||
// its own metatable with __mode "kv". 5.1 keeps it in the closure's upvalue;
|
||||
// here it lives in the registry under kValidKey, so newproxy is a plain C
|
||||
// function that goes through the registrars like every other global. Each
|
||||
// Lua state (game, glue, and every /reload) gets its own, made on first use.
|
||||
//
|
||||
// Registered on BOTH states (language-level helper, both are 5.0).
|
||||
|
||||
@@ -30,48 +33,70 @@ 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;
|
||||
}
|
||||
constexpr const char *kValidKey = "__classicapi_newproxy_metatables";
|
||||
|
||||
void Register() {
|
||||
void *L = Lua::State();
|
||||
if (L == nullptr)
|
||||
// Pushes the weak table of valid metatables, creating it on first use.
|
||||
void PushValidSet(void *L) {
|
||||
Lua::PushString(L, kValidKey);
|
||||
Lua::RawGet(L, Lua::REGISTRY_INDEX);
|
||||
if (Lua::Type(L, -1) == Lua::TYPE_TABLE)
|
||||
return;
|
||||
Lua::PushString(L, "newproxy");
|
||||
Lua::SetTop(L, -2);
|
||||
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);
|
||||
Lua::PushString(L, kValidKey);
|
||||
Lua::PushValue(L, -2);
|
||||
Lua::RawSet(L, Lua::REGISTRY_INDEX); // registry[kValidKey] = w
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Register};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&Register};
|
||||
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, at 3 ...
|
||||
PushValidSet(L);
|
||||
Lua::PushValue(L, 3);
|
||||
Lua::PushBoolean(L, 1);
|
||||
Lua::RawSet(L, -3); // ... marked valid: weak[m] = true
|
||||
Lua::SetTop(L, 3);
|
||||
} else {
|
||||
bool valid = false;
|
||||
if (Lua::GetMetatable(L, 1)) { // its metatable, at 3
|
||||
PushValidSet(L);
|
||||
Lua::PushValue(L, 3);
|
||||
Lua::RawGet(L, -2);
|
||||
valid = Lua::ToBoolean(L, -1) != 0;
|
||||
Lua::SetTop(L, 3);
|
||||
}
|
||||
if (!valid)
|
||||
Lua::ArgError(L, 1, "boolean or proxy expected");
|
||||
}
|
||||
Lua::SetMetatable(L, 2);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Opt("proxy", "any", "false",
|
||||
"False for no metatable, true for a new one, or another proxy to share its "
|
||||
"metatable."),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Req("proxy", "userdata", "An empty userdata."),
|
||||
};
|
||||
const Game::Doc::Function kNewproxy{"Returns a new empty userdata, with or without a metatable.",
|
||||
kArgs, kRets, "LuaGlobals"};
|
||||
|
||||
void RegisterInGame() { Lua::RegisterGlobalFunction("newproxy", &Script_newproxy, &kNewproxy); }
|
||||
void RegisterGlue() { Lua::RegisterGlueFunction("newproxy", &Script_newproxy, &kNewproxy); }
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterInGame};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterGlue};
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
+15
-2
@@ -71,12 +71,25 @@ int __fastcall Script_select(void *L) {
|
||||
return n - i;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kSelectArgs[] = {
|
||||
Game::Doc::Req("index", "number",
|
||||
"The first argument to return, negative to count back from the last, or "
|
||||
"\"#\" for the count."),
|
||||
Game::Doc::Vararg("any"),
|
||||
};
|
||||
const Game::Doc::Field kSelectRets[] = {
|
||||
Game::Doc::Vararg("any", "The arguments from index onward, or their count when index is \"#\"."),
|
||||
};
|
||||
const Game::Doc::Function kSelect{
|
||||
"Returns its extra arguments from a position onward, or the number of them.",
|
||||
kSelectArgs, kSelectRets, "LuaGlobals"};
|
||||
|
||||
void RegisterInGame() {
|
||||
Game::Lua::RegisterGlobalFunction("select", &Script_select);
|
||||
Game::Lua::RegisterGlobalFunction("select", &Script_select, &kSelect);
|
||||
}
|
||||
|
||||
void RegisterGlue() {
|
||||
Game::Lua::RegisterGlueFunction("select", &Script_select);
|
||||
Game::Lua::RegisterGlueFunction("select", &Script_select, &kSelect);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterInGame};
|
||||
|
||||
+109
-16
@@ -385,7 +385,7 @@ const Game::Doc::Field kStrlenutf8Rets[] = {
|
||||
Game::Doc::Req("length", "number", "The number of characters; strlen counts bytes."),
|
||||
};
|
||||
const Game::Doc::Function kStrlenutf8{
|
||||
"The number of characters in a UTF-8 string, not the number of bytes.",
|
||||
"Returns the number of characters in a UTF-8 string, not the number of bytes.",
|
||||
kStrlenutf8Args, kStrlenutf8Rets, "LuaGlobals"};
|
||||
|
||||
const Game::Doc::Field kStrcmputf8iArgs[] = {
|
||||
@@ -425,14 +425,107 @@ const Game::Doc::Function kStrconcat{
|
||||
"Joins every argument into one string with nothing between them.",
|
||||
kStrconcatArgs, kStrconcatRets, "LuaGlobals"};
|
||||
|
||||
// The `string.*` entries and their global twins share a descriptor; the
|
||||
// system names the global's, and the table entry takes the table's.
|
||||
const Game::Doc::Field kMatchArgs[] = {
|
||||
Game::Doc::Req("s", "string"),
|
||||
Game::Doc::Req("pattern", "string"),
|
||||
Game::Doc::Opt("init", "number", "1",
|
||||
"The position where the search starts; a negative one counts from the end."),
|
||||
};
|
||||
const Game::Doc::Field kMatchRets[] = {
|
||||
Game::Doc::Vararg("string",
|
||||
"The captures of the first match, or the whole match when the pattern has "
|
||||
"no captures; nil when nothing matches."),
|
||||
};
|
||||
const Game::Doc::Function kMatch{"Finds the first match of a pattern in a string.", kMatchArgs,
|
||||
kMatchRets, "LuaGlobals"};
|
||||
|
||||
const Game::Doc::Field kGmatchArgs[] = {
|
||||
Game::Doc::Req("s", "string"),
|
||||
Game::Doc::Req("pattern", "string"),
|
||||
};
|
||||
const Game::Doc::Field kGmatchRets[] = {
|
||||
Game::Doc::Req("iterator", "function",
|
||||
"Each call returns the captures of the next match, or the whole match when "
|
||||
"the pattern has no captures."),
|
||||
};
|
||||
const Game::Doc::Function kGmatch{
|
||||
"Returns an iterator over the matches of a pattern in a string, for a generic for loop.",
|
||||
kGmatchArgs, kGmatchRets, "LuaGlobals"};
|
||||
|
||||
const Game::Doc::Field kReverseArgs[] = {
|
||||
Game::Doc::Req("s", "string"),
|
||||
};
|
||||
const Game::Doc::Field kReverseRets[] = {
|
||||
Game::Doc::Req("reversed", "string"),
|
||||
};
|
||||
const Game::Doc::Function kReverse{"Returns a string with its bytes in reverse order.",
|
||||
kReverseArgs, kReverseRets, "LuaGlobals"};
|
||||
|
||||
const Game::Doc::Field kByteArgs[] = {
|
||||
Game::Doc::Req("s", "string"),
|
||||
Game::Doc::Opt("i", "number", "1",
|
||||
"The first position; a negative one counts back from the end."),
|
||||
Game::Doc::Opt("j", "number", nullptr, "The last position; i by default."),
|
||||
};
|
||||
const Game::Doc::Field kByteRets[] = {
|
||||
Game::Doc::Vararg("number", "The code of each byte; nothing when the range is empty."),
|
||||
};
|
||||
const Game::Doc::Function kByte{"Returns the numeric codes of the bytes from position i to j.",
|
||||
kByteArgs, kByteRets, "LuaGlobals"};
|
||||
|
||||
const Game::Doc::Field kSplitRets[] = {
|
||||
Game::Doc::Vararg("string", "The pieces, in order."),
|
||||
};
|
||||
const Game::Doc::Function kSplit{
|
||||
"Splits a string on delimiter characters and returns the pieces as separate values.",
|
||||
kStrsplittableArgs, kSplitRets, "LuaGlobals"};
|
||||
|
||||
const Game::Doc::Field kJoinArgs[] = {
|
||||
Game::Doc::Req("delimiter", "string"),
|
||||
Game::Doc::Vararg("string", "The pieces; a value that is not a string or number counts as "
|
||||
"an empty string."),
|
||||
};
|
||||
const Game::Doc::Field kJoinRets[] = {
|
||||
Game::Doc::Req("result", "string"),
|
||||
};
|
||||
const Game::Doc::Function kJoin{"Joins the pieces into one string with delimiter between them.",
|
||||
kJoinArgs, kJoinRets, "LuaGlobals"};
|
||||
|
||||
const Game::Doc::Field kTrimArgs[] = {
|
||||
Game::Doc::Req("str", "string"),
|
||||
Game::Doc::Opt("chars", "string", nullptr,
|
||||
"The characters to remove, as a plain set and not a pattern; whitespace by "
|
||||
"default."),
|
||||
};
|
||||
const Game::Doc::Field kTrimRets[] = {
|
||||
Game::Doc::Req("trimmed", "string"),
|
||||
};
|
||||
const Game::Doc::Function kTrim{"Removes the given characters from both ends of a string.",
|
||||
kTrimArgs, kTrimRets, "LuaGlobals"};
|
||||
|
||||
const Game::Doc::Field kStrreplaceArgs[] = {
|
||||
Game::Doc::Req("str", "string"),
|
||||
Game::Doc::Req("find", "string", "Plain text, not a pattern."),
|
||||
Game::Doc::Req("replace", "string"),
|
||||
};
|
||||
const Game::Doc::Field kStrreplaceRets[] = {
|
||||
Game::Doc::Req("result", "string"),
|
||||
Game::Doc::Req("count", "number", "The number of replacements; 0 when find is empty."),
|
||||
};
|
||||
const Game::Doc::Function kStrreplace{
|
||||
"Replaces every occurrence of a plain substring in a string.",
|
||||
kStrreplaceArgs, kStrreplaceRets, "LuaGlobals", true};
|
||||
|
||||
void RegisterFns() {
|
||||
Game::Lua::RegisterTableFunction("string", "match", &Script_string_match);
|
||||
Game::Lua::RegisterTableFunction("string", "gmatch", Script_string_gmatch);
|
||||
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", "match", &Script_string_match, &kMatch);
|
||||
Game::Lua::RegisterTableFunction("string", "gmatch", Script_string_gmatch, &kGmatch);
|
||||
Game::Lua::RegisterTableFunction("string", "reverse", &Script_string_reverse, &kReverse);
|
||||
Game::Lua::RegisterTableFunction("string", "byte", &Script_string_byte, &kByte);
|
||||
Game::Lua::RegisterTableFunction("string", "split", &Script_strsplit, &kSplit);
|
||||
Game::Lua::RegisterTableFunction("string", "trim", &Script_strtrim, &kTrim);
|
||||
Game::Lua::RegisterTableFunction("string", "join", &Script_strjoin, &kJoin);
|
||||
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 (`gmatch = str.gmatch`, `strmatch =
|
||||
@@ -443,15 +536,15 @@ void RegisterFns() {
|
||||
// half is what lets `(" x "):trim()` work and libraries capture it at
|
||||
// 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("gmatch", "string", "gmatch", &kGmatch);
|
||||
Game::Lua::RegisterGlobalAlias("strmatch", "string", "match", &kMatch);
|
||||
Game::Lua::RegisterGlobalAlias("strsplit", "string", "split", &kSplit);
|
||||
Game::Lua::RegisterGlobalAlias("strjoin", "string", "join", &kJoin);
|
||||
Game::Lua::RegisterGlobalAlias("strtrim", "string", "trim", &kTrim);
|
||||
Game::Lua::RegisterGlobalAlias("strrev", "string", "reverse", &kReverse);
|
||||
Game::Lua::RegisterGlobalAlias("strbyte", "string", "byte", &kByte);
|
||||
Game::Lua::RegisterGlobalAlias("strconcat", "string", "concat", &kStrconcat);
|
||||
Game::Lua::RegisterGlobalFunction("strreplace", &Script_strreplace);
|
||||
Game::Lua::RegisterGlobalFunction("strreplace", &Script_strreplace, &kStrreplace);
|
||||
Game::Lua::RegisterGlobalFunction("strlenutf8", &Script_strlenutf8, &kStrlenutf8);
|
||||
Game::Lua::RegisterGlobalFunction("strcmputf8i", &Script_strcmputf8i, &kStrcmputf8i);
|
||||
Game::Lua::RegisterGlobalFunction("strsplittable", &Script_strsplittable,
|
||||
|
||||
+15
-2
@@ -70,12 +70,25 @@ int __fastcall Script_unpack(void *L) {
|
||||
return n;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kUnpackArgs[] = {
|
||||
Game::Doc::Req("list", "table"),
|
||||
Game::Doc::Opt("i", "number", "1", "The first index to return."),
|
||||
Game::Doc::Opt("j", "number", nullptr,
|
||||
"The last index to return; the length of list, as table.getn gives it, by "
|
||||
"default."),
|
||||
};
|
||||
const Game::Doc::Field kUnpackRets[] = {
|
||||
Game::Doc::Vararg("any", "list[i] through list[j]; nothing when i is greater than j."),
|
||||
};
|
||||
const Game::Doc::Function kUnpack{"Returns the elements of a table from index i to index j.",
|
||||
kUnpackArgs, kUnpackRets, "LuaGlobals"};
|
||||
|
||||
void RegisterInGame() {
|
||||
Game::Lua::RegisterGlobalFunction("unpack", &Script_unpack);
|
||||
Game::Lua::RegisterGlobalFunction("unpack", &Script_unpack, &kUnpack);
|
||||
}
|
||||
|
||||
void RegisterGlue() {
|
||||
Game::Lua::RegisterGlueFunction("unpack", &Script_unpack);
|
||||
Game::Lua::RegisterGlueFunction("unpack", &Script_unpack, &kUnpack);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterInGame};
|
||||
|
||||
+16
-2
@@ -78,12 +78,26 @@ int __fastcall Script_xpcall(void *L) {
|
||||
return Game::Lua::GetTop(L) - 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kXpcallArgs[] = {
|
||||
Game::Doc::Req("f", "function"),
|
||||
Game::Doc::Req("msgh", "function",
|
||||
"The message handler, called with the error when f fails."),
|
||||
Game::Doc::Vararg("any", "The arguments for f."),
|
||||
};
|
||||
const Game::Doc::Field kXpcallRets[] = {
|
||||
Game::Doc::Req("success", "bool", "False when f raised an error."),
|
||||
Game::Doc::Vararg("any", "The results of f, or the return of msgh after an error."),
|
||||
};
|
||||
const Game::Doc::Function kXpcall{
|
||||
"Calls a function with the given arguments in protected mode, with a message handler for errors.",
|
||||
kXpcallArgs, kXpcallRets, "LuaGlobals"};
|
||||
|
||||
void RegisterInGame() {
|
||||
Game::Lua::RegisterGlobalFunction("xpcall", &Script_xpcall);
|
||||
Game::Lua::RegisterGlobalFunction("xpcall", &Script_xpcall, &kXpcall);
|
||||
}
|
||||
|
||||
void RegisterGlue() {
|
||||
Game::Lua::RegisterGlueFunction("xpcall", &Script_xpcall);
|
||||
Game::Lua::RegisterGlueFunction("xpcall", &Script_xpcall, &kXpcall);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterInGame};
|
||||
|
||||
+131
-11
@@ -414,20 +414,140 @@ const Game::Doc::Function kGetBindingByKey{"Returns the command that a key press
|
||||
kGetBindingByKeyArgs, kGetBindingByKeyRets,
|
||||
"BindingsGlobals"};
|
||||
|
||||
const Game::Doc::Field kSetBindingRets[] = {
|
||||
Game::Doc::Opt("ok", "number", nullptr, "1 when the binding changed, otherwise nil."),
|
||||
};
|
||||
const Game::Doc::Field kSetBindingSpellArgs[] = {
|
||||
Game::Doc::Req("key", "string"),
|
||||
Game::Doc::Req("spell", "string", "A spell name."),
|
||||
};
|
||||
const Game::Doc::Function kSetBindingSpell{
|
||||
"Binds a key in the current binding set to cast a spell.", kSetBindingSpellArgs,
|
||||
kSetBindingRets, "BindingsGlobals"};
|
||||
const Game::Doc::Field kSetBindingItemArgs[] = {
|
||||
Game::Doc::Req("key", "string"),
|
||||
Game::Doc::Req("item", "string", "An item name."),
|
||||
};
|
||||
const Game::Doc::Function kSetBindingItem{
|
||||
"Binds a key in the current binding set to use an item.", kSetBindingItemArgs,
|
||||
kSetBindingRets, "BindingsGlobals"};
|
||||
const Game::Doc::Field kSetBindingMacroArgs[] = {
|
||||
Game::Doc::Req("key", "string"),
|
||||
Game::Doc::Req("macro", "string", "A macro name, or a macro index as a number."),
|
||||
};
|
||||
const Game::Doc::Function kSetBindingMacro{
|
||||
"Binds a key in the current binding set to run a macro.", kSetBindingMacroArgs,
|
||||
kSetBindingRets, "BindingsGlobals"};
|
||||
const Game::Doc::Field kSetBindingClickArgs[] = {
|
||||
Game::Doc::Req("key", "string"),
|
||||
Game::Doc::Req("buttonName", "string", "The global name of the button."),
|
||||
Game::Doc::Opt("mouseButton", "string", "\"LeftButton\""),
|
||||
};
|
||||
const Game::Doc::Function kSetBindingClick{
|
||||
"Binds a key in the current binding set to click a button.", kSetBindingClickArgs,
|
||||
kSetBindingRets, "BindingsGlobals"};
|
||||
|
||||
const Game::Doc::Field kSetOverrideBindingArgs[] = {
|
||||
Game::Doc::Req("owner", "SimpleFrame"),
|
||||
Game::Doc::Req("isPriority", "bool"),
|
||||
Game::Doc::Req("key", "string",
|
||||
"Not case sensitive; write the modifiers in the order ALT-CTRL-SHIFT."),
|
||||
Game::Doc::Opt("command", "string", nullptr,
|
||||
"Nil or an empty string removes the owner's override for the key."),
|
||||
};
|
||||
const Game::Doc::Function kSetOverrideBinding{
|
||||
"Sets a temporary binding for a key that replaces its saved binding until it is removed.",
|
||||
kSetOverrideBindingArgs, {}, "BindingsGlobals"};
|
||||
const Game::Doc::Field kSetOverrideBindingSpellArgs[] = {
|
||||
Game::Doc::Req("owner", "SimpleFrame"),
|
||||
Game::Doc::Req("isPriority", "bool"),
|
||||
Game::Doc::Req("key", "string"),
|
||||
Game::Doc::Req("spell", "string", "A spell name."),
|
||||
};
|
||||
const Game::Doc::Function kSetOverrideBindingSpell{
|
||||
"Sets a temporary binding for a key that casts a spell.", kSetOverrideBindingSpellArgs, {},
|
||||
"BindingsGlobals"};
|
||||
const Game::Doc::Field kSetOverrideBindingItemArgs[] = {
|
||||
Game::Doc::Req("owner", "SimpleFrame"),
|
||||
Game::Doc::Req("isPriority", "bool"),
|
||||
Game::Doc::Req("key", "string"),
|
||||
Game::Doc::Req("item", "string", "An item name."),
|
||||
};
|
||||
const Game::Doc::Function kSetOverrideBindingItem{
|
||||
"Sets a temporary binding for a key that uses an item.", kSetOverrideBindingItemArgs, {},
|
||||
"BindingsGlobals"};
|
||||
const Game::Doc::Field kSetOverrideBindingMacroArgs[] = {
|
||||
Game::Doc::Req("owner", "SimpleFrame"),
|
||||
Game::Doc::Req("isPriority", "bool"),
|
||||
Game::Doc::Req("key", "string"),
|
||||
Game::Doc::Req("macro", "string", "A macro name, or a macro index as a number."),
|
||||
};
|
||||
const Game::Doc::Function kSetOverrideBindingMacro{
|
||||
"Sets a temporary binding for a key that runs a macro.", kSetOverrideBindingMacroArgs, {},
|
||||
"BindingsGlobals"};
|
||||
const Game::Doc::Field kSetOverrideBindingClickArgs[] = {
|
||||
Game::Doc::Req("owner", "SimpleFrame"),
|
||||
Game::Doc::Req("isPriority", "bool"),
|
||||
Game::Doc::Req("key", "string"),
|
||||
Game::Doc::Req("buttonName", "string", "The global name of the button."),
|
||||
Game::Doc::Opt("mouseButton", "string", "\"LeftButton\""),
|
||||
};
|
||||
const Game::Doc::Function kSetOverrideBindingClick{
|
||||
"Sets a temporary binding for a key that clicks a button.", kSetOverrideBindingClickArgs,
|
||||
{}, "BindingsGlobals"};
|
||||
const Game::Doc::Field kClearOverrideBindingsArgs[] = {
|
||||
Game::Doc::Req("owner", "SimpleFrame"),
|
||||
};
|
||||
const Game::Doc::Function kClearOverrideBindings{"Removes all override bindings of an owner.",
|
||||
kClearOverrideBindingsArgs, {},
|
||||
"BindingsGlobals"};
|
||||
|
||||
const Game::Doc::Field kGetBindingActionArgs[] = {
|
||||
Game::Doc::Req("key", "string"),
|
||||
Game::Doc::Opt("checkOverride", "bool", "false",
|
||||
"True returns an active override binding first."),
|
||||
};
|
||||
const Game::Doc::Field kGetBindingActionRets[] = {
|
||||
Game::Doc::Req("action", "string", "An empty string when nothing is bound."),
|
||||
};
|
||||
const Game::Doc::Function kGetBindingAction{"Returns the command bound to a key.",
|
||||
kGetBindingActionArgs, kGetBindingActionRets,
|
||||
"BindingsGlobals"};
|
||||
|
||||
// The engine's binding sets (Offsets::BINDING_SET_*), as retail names them.
|
||||
const Game::Lua::EnumIntegerEntry kBindingSet[] = {
|
||||
{"Default", Offsets::BINDING_SET_DEFAULT},
|
||||
{"Account", Offsets::BINDING_SET_ACCOUNT},
|
||||
{"Character", Offsets::BINDING_SET_CHARACTER},
|
||||
{"Current", Offsets::BINDING_SET_CURRENT},
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "BindingSet", kBindingSet, "KeyBindings");
|
||||
Game::Lua::RegisterGlobalFunction("GetBindingByKey", &Script_GetBindingByKey,
|
||||
&kGetBindingByKey);
|
||||
Game::Lua::RegisterGlobalFunction("SetBindingSpell", &Script_SetBindingSpell);
|
||||
Game::Lua::RegisterGlobalFunction("SetBindingItem", &Script_SetBindingItem);
|
||||
Game::Lua::RegisterGlobalFunction("SetBindingMacro", &Script_SetBindingMacro);
|
||||
Game::Lua::RegisterGlobalFunction("SetBindingClick", &Script_SetBindingClick);
|
||||
Game::Lua::RegisterGlobalFunction("SetOverrideBinding", &Script_SetOverrideBinding);
|
||||
Game::Lua::RegisterGlobalFunction("SetOverrideBindingSpell", &Script_SetOverrideBindingSpell);
|
||||
Game::Lua::RegisterGlobalFunction("SetOverrideBindingItem", &Script_SetOverrideBindingItem);
|
||||
Game::Lua::RegisterGlobalFunction("SetOverrideBindingMacro", &Script_SetOverrideBindingMacro);
|
||||
Game::Lua::RegisterGlobalFunction("SetOverrideBindingClick", &Script_SetOverrideBindingClick);
|
||||
Game::Lua::RegisterGlobalFunction("ClearOverrideBindings", &Script_ClearOverrideBindings);
|
||||
Game::Lua::RegisterGlobalFunction("GetBindingAction", &Script_GetBindingAction);
|
||||
Game::Lua::RegisterGlobalFunction("SetBindingSpell", &Script_SetBindingSpell,
|
||||
&kSetBindingSpell);
|
||||
Game::Lua::RegisterGlobalFunction("SetBindingItem", &Script_SetBindingItem,
|
||||
&kSetBindingItem);
|
||||
Game::Lua::RegisterGlobalFunction("SetBindingMacro", &Script_SetBindingMacro,
|
||||
&kSetBindingMacro);
|
||||
Game::Lua::RegisterGlobalFunction("SetBindingClick", &Script_SetBindingClick,
|
||||
&kSetBindingClick);
|
||||
Game::Lua::RegisterGlobalFunction("SetOverrideBinding", &Script_SetOverrideBinding,
|
||||
&kSetOverrideBinding);
|
||||
Game::Lua::RegisterGlobalFunction("SetOverrideBindingSpell", &Script_SetOverrideBindingSpell,
|
||||
&kSetOverrideBindingSpell);
|
||||
Game::Lua::RegisterGlobalFunction("SetOverrideBindingItem", &Script_SetOverrideBindingItem,
|
||||
&kSetOverrideBindingItem);
|
||||
Game::Lua::RegisterGlobalFunction("SetOverrideBindingMacro", &Script_SetOverrideBindingMacro,
|
||||
&kSetOverrideBindingMacro);
|
||||
Game::Lua::RegisterGlobalFunction("SetOverrideBindingClick", &Script_SetOverrideBindingClick,
|
||||
&kSetOverrideBindingClick);
|
||||
Game::Lua::RegisterGlobalFunction("ClearOverrideBindings", &Script_ClearOverrideBindings,
|
||||
&kClearOverrideBindings);
|
||||
Game::Lua::RegisterGlobalFunction("GetBindingAction", &Script_GetBindingAction,
|
||||
&kGetBindingAction);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "bindings/ModifiedClick.h"
|
||||
#include "frame/ClickEvents.h"
|
||||
#include "frame/ClickFrame.h"
|
||||
#include "item/Arg.h"
|
||||
#include "item/Location.h"
|
||||
@@ -91,23 +92,6 @@ void UseItem(const char *name) {
|
||||
Item::Use::UseItem(found.item, ModifiedTarget());
|
||||
}
|
||||
|
||||
// StringToMouseButton: the click mask for a button name, 0 for any other
|
||||
// (the click vmethod then names it "UNKNOWN").
|
||||
uint32_t MaskForButton(const char *button) {
|
||||
static const struct {
|
||||
uintptr_t name;
|
||||
uint32_t mask;
|
||||
} kButtons[] = {
|
||||
{Offsets::VAR_BUTTON_NAME_LEFT, 0x01}, {Offsets::VAR_BUTTON_NAME_RIGHT, 0x04},
|
||||
{Offsets::VAR_BUTTON_NAME_MIDDLE, 0x02}, {Offsets::VAR_BUTTON_NAME_4, 0x08},
|
||||
{Offsets::VAR_BUTTON_NAME_5, 0x10},
|
||||
};
|
||||
for (const auto &entry : kButtons)
|
||||
if (_stricmp(reinterpret_cast<const char *>(entry.name), button) == 0)
|
||||
return entry.mask;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The frame's OnMouseDown / OnMouseUp, fired as the engine fires them.
|
||||
void FireMouseScript(void *frame, uintptr_t slotOffset, const char *button) {
|
||||
using FireVariadic_t = void(__cdecl *)(void *frame, uint32_t *slotPtr, const char *fmt, ...);
|
||||
@@ -149,7 +133,7 @@ void VirtualMouseDown(void *frame, const char *button) {
|
||||
ButtonState(frame) == Offsets::BUTTON_STATE_DISABLED)
|
||||
return;
|
||||
if (Game::Read<uint32_t>(frame, Offsets::OFF_BUTTON_CLICK_MASK) & 0xFF)
|
||||
InClick([&] { CallButton(frame, Offsets::VTBL_BUTTON_CLICK, MaskForButton(button)); });
|
||||
InClick([&] { CallButton(frame, Offsets::VTBL_BUTTON_CLICK, Frame::ClickEvents::MaskForButtonName(button)); });
|
||||
if (!StateLocked(frame) && ButtonState(frame) != Offsets::BUTTON_STATE_DISABLED)
|
||||
CallButton(frame, Offsets::VTBL_BUTTON_SET_STATE, Offsets::BUTTON_STATE_PUSHED);
|
||||
}
|
||||
@@ -164,7 +148,7 @@ void VirtualMouseUp(void *frame, const char *button) {
|
||||
InClick([&] {
|
||||
const uint32_t now = Time::Clock::NowMs();
|
||||
auto &last = Game::Ref<uint32_t>(frame, Offsets::OFF_BUTTON_LAST_CLICK_MS);
|
||||
const uint32_t mask = MaskForButton(button);
|
||||
const uint32_t mask = Frame::ClickEvents::MaskForButtonName(button);
|
||||
if (Game::Read<uint32_t>(frame, Offsets::OFF_BUTTON_ONDOUBLECLICK_HANDLER) != 0 &&
|
||||
last != 0 && Time::Clock::Elapsed(last, now) <= Offsets::BUTTON_DOUBLE_CLICK_MS) {
|
||||
CallButton(frame, Offsets::VTBL_BUTTON_DOUBLE_CLICK, mask);
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
#include "Inject.h"
|
||||
#include "Common.h"
|
||||
#include "Offsets.h"
|
||||
#include "storm/Memory.h"
|
||||
|
||||
#include "embed/Resource.h"
|
||||
#include "embedded_resources.h"
|
||||
@@ -213,8 +214,6 @@ const char *FindSplicePosition(const char *content, size_t size,
|
||||
return FindOutsideComment(content, end, "</Bindings>");
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -257,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.
|
||||
@@ -277,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);
|
||||
|
||||
+22
-2
@@ -141,11 +141,31 @@ int __fastcall Script_SetSavedCharacterOrder(void *L) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kRealmArgs[] = {
|
||||
Game::Doc::Req("realm", "string"),
|
||||
};
|
||||
const Game::Doc::Field kGetSavedCharacterOrderRets[] = {
|
||||
Game::Doc::Req("order", "string", "Empty when no order is saved for the realm."),
|
||||
};
|
||||
const Game::Doc::Function kGetSavedCharacterOrder{
|
||||
"Returns the character order saved for a realm on this account.", kRealmArgs,
|
||||
kGetSavedCharacterOrderRets, "CharacterListGlobals", true};
|
||||
|
||||
const Game::Doc::Field kSetSavedCharacterOrderArgs[] = {
|
||||
Game::Doc::Req("realm", "string"),
|
||||
Game::Doc::Req("order", "string",
|
||||
"Any text without line breaks, such as names joined by \"|\"; empty clears "
|
||||
"the saved order."),
|
||||
};
|
||||
const Game::Doc::Function kSetSavedCharacterOrder{
|
||||
"Saves a character order for a realm on this account.", kSetSavedCharacterOrderArgs, {},
|
||||
"CharacterListGlobals", true};
|
||||
|
||||
void RegisterGlue() {
|
||||
Game::Lua::RegisterGlueFunction("GetSavedCharacterOrder",
|
||||
&Script_GetSavedCharacterOrder);
|
||||
&Script_GetSavedCharacterOrder, &kGetSavedCharacterOrder);
|
||||
Game::Lua::RegisterGlueFunction("SetSavedCharacterOrder",
|
||||
&Script_SetSavedCharacterOrder);
|
||||
&Script_SetSavedCharacterOrder, &kSetSavedCharacterOrder);
|
||||
}
|
||||
|
||||
const Game::GlueModuleAutoRegister _autoreg{&RegisterGlue};
|
||||
|
||||
@@ -43,14 +43,14 @@ int __fastcall Script_IsChatDND(void *L) {
|
||||
}
|
||||
|
||||
const Game::Doc::Field kAfkRets[] = {
|
||||
Game::Doc::Req("isAFK", "boolean", nullptr),
|
||||
Game::Doc::Req("isAFK", "bool", nullptr),
|
||||
};
|
||||
const Game::Doc::Function kIsChatAFK{
|
||||
"Returns whether the player is marked AFK. Changes as soon as /afk is typed or AFK is cleared.",
|
||||
"Returns whether the player is marked AFK, changing as soon as /afk is typed or cleared.",
|
||||
{}, kAfkRets, "ChatGlobals"};
|
||||
|
||||
const Game::Doc::Field kDndRets[] = {
|
||||
Game::Doc::Req("isDND", "boolean", nullptr),
|
||||
Game::Doc::Req("isDND", "bool", nullptr),
|
||||
};
|
||||
const Game::Doc::Function kIsChatDND{
|
||||
"Returns whether the player is marked DND.", {}, kDndRets, "ChatGlobals"};
|
||||
|
||||
+11
-2
@@ -197,13 +197,21 @@ const Game::Doc::Field kChatEventRets[] = {
|
||||
Game::Doc::Opt("channelBaseName", "string"),
|
||||
Game::Doc::Opt("unused", "number"),
|
||||
Game::Doc::Opt("lineID", "number"),
|
||||
Game::Doc::Opt("guid", "string"),
|
||||
Game::Doc::Opt("guid", "WOWGUID"),
|
||||
};
|
||||
const Game::Doc::Function kGetCurrentChatEvent{
|
||||
"Returns the name and arguments of the CHAT_MSG_* event being handled now, or nothing "
|
||||
"outside one.",
|
||||
{}, kChatEventRets, "ChatGlobals", true};
|
||||
|
||||
const Game::Doc::Field kGetCurrentChatGUIDRets[] = {
|
||||
Game::Doc::Opt("guid", "WOWGUID", nullptr,
|
||||
"Nil when the sender is not a player, or outside a chat event."),
|
||||
};
|
||||
const Game::Doc::Function kGetCurrentChatGUID{
|
||||
"Returns the sender GUID of the CHAT_MSG_* event being handled now.", {},
|
||||
kGetCurrentChatGUIDRets, "ChatGlobals", true};
|
||||
|
||||
// Once per process; a patched site no longer reaches FUN_FIRE_EVENT, so a
|
||||
// second pass would refuse anyway.
|
||||
bool g_applied = false;
|
||||
@@ -227,7 +235,8 @@ void Apply() {
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetCurrentChatGUID", &Script_GetCurrentChatGUID);
|
||||
Game::Lua::RegisterGlobalFunction("GetCurrentChatGUID", &Script_GetCurrentChatGUID,
|
||||
&kGetCurrentChatGUID);
|
||||
Game::Lua::RegisterGlobalFunction("GetCurrentChatEvent", &Script_GetCurrentChatEvent,
|
||||
&kGetCurrentChatEvent);
|
||||
Apply();
|
||||
|
||||
@@ -52,8 +52,7 @@ const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("message", "string", "The text of the line."),
|
||||
};
|
||||
const Game::Doc::Function kSendSystemMessage{
|
||||
"Shows a line in chat as a system message, only on your own client. It fires "
|
||||
"CHAT_MSG_SYSTEM with the text as arg1.",
|
||||
"Shows a line in chat as a system message on your own client and fires CHAT_MSG_SYSTEM.",
|
||||
kArgs, {}, "ChatGlobals"};
|
||||
|
||||
} // namespace
|
||||
|
||||
+11
-1
@@ -63,9 +63,19 @@ static int __fastcall Script_GetAllChatBubbles(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
static const Game::Doc::Field kGetAllChatBubblesArgs[] = {
|
||||
Game::Doc::Opt("includeForbidden", "bool", "false", "Ignored."),
|
||||
};
|
||||
static const Game::Doc::Field kGetAllChatBubblesRets[] = {
|
||||
Game::Doc::Req("chatBubbles", "table", "An array of the chat bubble frames now showing."),
|
||||
};
|
||||
static const Game::Doc::Function kGetAllChatBubbles{
|
||||
"Returns the speech bubbles above characters that are now showing.", kGetAllChatBubblesArgs,
|
||||
kGetAllChatBubblesRets};
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_ChatBubbles", "GetAllChatBubbles",
|
||||
&Script_GetAllChatBubbles);
|
||||
&Script_GetAllChatBubbles, &kGetAllChatBubbles);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -70,7 +70,7 @@ const Game::Doc::Field kRets[] = {
|
||||
"The color for that class, or nil when the name is not a class."),
|
||||
};
|
||||
const Game::Doc::Function kGetClassColor{
|
||||
"The color a class is drawn in.", kArgs, kRets};
|
||||
"Returns the color a class is drawn in.", kArgs, kRets};
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
+15
-1
@@ -74,11 +74,25 @@ int __fastcall Script_FillLocalizedClassList(void *L) {
|
||||
return 1; // return the same table the caller passed in
|
||||
}
|
||||
|
||||
// --- Documentation ----------------------------------------------------------
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("classTable", "table", "Gets a [classFile] = localized name pair per class."),
|
||||
Game::Doc::Opt("isFemale", "bool", "false", "Ignored; the names are the same either way."),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Req("classTable", "table", "The same table."),
|
||||
};
|
||||
const Game::Doc::Function kFillLocalizedClassList{
|
||||
"Fills a table with the localized name of every class, keyed by class file name.",
|
||||
kArgs, kRets, "UnitGlobals"};
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("FillLocalizedClassList",
|
||||
&Script_FillLocalizedClassList);
|
||||
&Script_FillLocalizedClassList,
|
||||
&kFillLocalizedClassList);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
+20
-3
@@ -104,21 +104,38 @@ int __fastcall Script_CopyToClipboard(void *L) {
|
||||
const bool removeMarkup = Game::Lua::ToBoolean(L, 2) != 0;
|
||||
|
||||
int copiedLen;
|
||||
bool copied;
|
||||
if (removeMarkup) {
|
||||
const std::string stripped = StripMarkup(text, len);
|
||||
copiedLen = static_cast<int>(stripped.size());
|
||||
SetClipboardUtf8(stripped.data(), copiedLen);
|
||||
copied = SetClipboardUtf8(stripped.data(), copiedLen);
|
||||
} else {
|
||||
copiedLen = static_cast<int>(len);
|
||||
SetClipboardUtf8(text, copiedLen);
|
||||
copied = SetClipboardUtf8(text, copiedLen);
|
||||
}
|
||||
if (!copied)
|
||||
copiedLen = 0;
|
||||
|
||||
Game::Lua::PushNumber(L, static_cast<double>(copiedLen));
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("text", "cstring"),
|
||||
Game::Doc::Opt("removeMarkup", "bool", "false",
|
||||
"True removes colors, links, textures and other escape codes, as Ctrl+C "
|
||||
"in an edit box does."),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Req("length", "number", "The length of the copied text in bytes; 0 when the clipboard "
|
||||
"could not be written."),
|
||||
};
|
||||
const Game::Doc::Function kCopyToClipboard{"Copies text to the system clipboard.", kArgs, kRets,
|
||||
"OsGlobals"};
|
||||
|
||||
void RegisterFns() {
|
||||
Game::Lua::RegisterGlobalFunction("CopyToClipboard", &Script_CopyToClipboard);
|
||||
Game::Lua::RegisterGlobalFunction("CopyToClipboard", &Script_CopyToClipboard,
|
||||
&kCopyToClipboard);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterFns};
|
||||
|
||||
+88
-8
@@ -264,23 +264,103 @@ int __fastcall Script_WrapTextInColorCode(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---- documentation ----------------------------------------------------------
|
||||
|
||||
const Game::Doc::Field kRgbArgs[] = {
|
||||
Game::Doc::Req("rgbR", "number", "From 0 to 1."),
|
||||
Game::Doc::Req("rgbG", "number"),
|
||||
Game::Doc::Req("rgbB", "number"),
|
||||
};
|
||||
const Game::Doc::Field kRgbRets[] = {
|
||||
Game::Doc::Req("rgbR", "number", "From 0 to 1."),
|
||||
Game::Doc::Req("rgbG", "number"),
|
||||
Game::Doc::Req("rgbB", "number"),
|
||||
};
|
||||
const Game::Doc::Field kHsvArgs[] = {
|
||||
Game::Doc::Req("hsvH", "number", "Degrees from 0 to 360; -1 means no hue."),
|
||||
Game::Doc::Req("hsvS", "number", "From 0 to 1."),
|
||||
Game::Doc::Req("hsvV", "number", "From 0 to 1."),
|
||||
};
|
||||
const Game::Doc::Field kHsvRets[] = {
|
||||
Game::Doc::Req("hsvH", "number", "Degrees from 0 to 360, or -1 for a gray."),
|
||||
Game::Doc::Req("hsvS", "number"),
|
||||
Game::Doc::Req("hsvV", "number"),
|
||||
};
|
||||
const Game::Doc::Field kHslArgs[] = {
|
||||
Game::Doc::Req("hslH", "number", "Degrees from 0 to 360; -1 means no hue."),
|
||||
Game::Doc::Req("hslS", "number", "From 0 to 1."),
|
||||
Game::Doc::Req("hslL", "number", "From 0 to 1."),
|
||||
};
|
||||
const Game::Doc::Field kHslRets[] = {
|
||||
Game::Doc::Req("hslH", "number", "The hue, unchanged."),
|
||||
Game::Doc::Req("hslS", "number"),
|
||||
Game::Doc::Req("hslL", "number"),
|
||||
};
|
||||
const Game::Doc::Field kHsvFromHslRets[] = {
|
||||
Game::Doc::Req("hsvH", "number", "The hue, unchanged."),
|
||||
Game::Doc::Req("hsvS", "number"),
|
||||
Game::Doc::Req("hsvV", "number"),
|
||||
};
|
||||
|
||||
const Game::Doc::Function kConvertRGBToHSV{"Converts an RGB color to hue, saturation and value.",
|
||||
kRgbArgs, kHsvRets};
|
||||
const Game::Doc::Function kConvertHSVToRGB{"Converts a hue, saturation and value color to RGB.",
|
||||
kHsvArgs, kRgbRets};
|
||||
const Game::Doc::Function kConvertHSVToHSL{
|
||||
"Converts a hue, saturation and value color to hue, saturation and lightness.", kHsvArgs,
|
||||
kHslRets};
|
||||
const Game::Doc::Function kConvertHSLToHSV{
|
||||
"Converts a hue, saturation and lightness color to hue, saturation and value.", kHslArgs,
|
||||
kHsvFromHslRets};
|
||||
const Game::Doc::Function kConvertHSLToRGB{
|
||||
"Converts a hue, saturation and lightness color to RGB.", kHslArgs, kRgbRets, nullptr, true};
|
||||
|
||||
const Game::Doc::Field kColorArg[] = {
|
||||
Game::Doc::Req("color", "colorRGBA",
|
||||
"A table with r, g and b from 0 to 1, and an optional a that defaults to 1."),
|
||||
};
|
||||
const Game::Doc::Field kTextColorCodeRets[] = {
|
||||
Game::Doc::Req("textColorCode", "string", "Eight hex digits, alpha first, without |c."),
|
||||
};
|
||||
const Game::Doc::Function kGenerateTextColorCode{"Returns the hex color code of a color.",
|
||||
kColorArg, kTextColorCodeRets};
|
||||
|
||||
const Game::Doc::Field kWrapTextInColorArgs[] = {
|
||||
Game::Doc::Req("text", "cstring"),
|
||||
Game::Doc::Req("color", "colorRGBA",
|
||||
"A table with r, g and b from 0 to 1, and an optional a that defaults to 1."),
|
||||
};
|
||||
const Game::Doc::Field kColoredTextRets[] = {
|
||||
Game::Doc::Req("coloredText", "string"),
|
||||
};
|
||||
const Game::Doc::Function kWrapTextInColor{"Returns text wrapped in the color code of a color.",
|
||||
kWrapTextInColorArgs, kColoredTextRets};
|
||||
|
||||
const Game::Doc::Field kWrapTextInColorCodeArgs[] = {
|
||||
Game::Doc::Req("text", "cstring"),
|
||||
Game::Doc::Req("textColorCode", "cstring",
|
||||
"Eight hex digits, as GenerateTextColorCode returns."),
|
||||
};
|
||||
const Game::Doc::Function kWrapTextInColorCode{"Returns text wrapped in a hex color code.",
|
||||
kWrapTextInColorCodeArgs, kColoredTextRets};
|
||||
|
||||
void RegisterFns() {
|
||||
Game::Lua::RegisterTableFunction("C_ColorUtil", "ConvertRGBToHSV",
|
||||
&Script_ConvertRGBToHSV);
|
||||
&Script_ConvertRGBToHSV, &kConvertRGBToHSV);
|
||||
Game::Lua::RegisterTableFunction("C_ColorUtil", "ConvertHSVToRGB",
|
||||
&Script_ConvertHSVToRGB);
|
||||
&Script_ConvertHSVToRGB, &kConvertHSVToRGB);
|
||||
Game::Lua::RegisterTableFunction("C_ColorUtil", "ConvertHSVToHSL",
|
||||
&Script_ConvertHSVToHSL);
|
||||
&Script_ConvertHSVToHSL, &kConvertHSVToHSL);
|
||||
Game::Lua::RegisterTableFunction("C_ColorUtil", "ConvertHSLToHSV",
|
||||
&Script_ConvertHSLToHSV);
|
||||
&Script_ConvertHSLToHSV, &kConvertHSLToHSV);
|
||||
Game::Lua::RegisterTableFunction("C_ColorUtil", "ConvertHSLToRGB",
|
||||
&Script_ConvertHSLToRGB);
|
||||
&Script_ConvertHSLToRGB, &kConvertHSLToRGB);
|
||||
Game::Lua::RegisterTableFunction("C_ColorUtil", "GenerateTextColorCode",
|
||||
&Script_GenerateTextColorCode);
|
||||
&Script_GenerateTextColorCode, &kGenerateTextColorCode);
|
||||
Game::Lua::RegisterTableFunction("C_ColorUtil", "WrapTextInColor",
|
||||
&Script_WrapTextInColor);
|
||||
&Script_WrapTextInColor, &kWrapTextInColor);
|
||||
Game::Lua::RegisterTableFunction("C_ColorUtil", "WrapTextInColorCode",
|
||||
&Script_WrapTextInColorCode);
|
||||
&Script_WrapTextInColorCode, &kWrapTextInColorCode);
|
||||
}
|
||||
|
||||
// Pure helpers — useful on both the in-game and glue states.
|
||||
|
||||
+15
-2
@@ -100,9 +100,22 @@ static int __fastcall Script_StopAttack(void *L) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// --- Documentation ----------------------------------------------------------
|
||||
|
||||
static const Game::Doc::Field kStartAttackArgs[] = {
|
||||
Game::Doc::Opt("target", "UnitToken", nullptr,
|
||||
"The unit to attack; your target when left out or not a unit token."),
|
||||
};
|
||||
static const Game::Doc::Function kStartAttack{
|
||||
"Starts your melee auto-attack; unlike AttackTarget it never turns the attack off.",
|
||||
kStartAttackArgs, {}, "CombatGlobals"};
|
||||
static const Game::Doc::Function kStopAttack{
|
||||
"Stops your melee auto-attack; does nothing when you are not attacking.", {}, {},
|
||||
"CombatGlobals"};
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("StartAttack", &Script_StartAttack);
|
||||
Game::Lua::RegisterGlobalFunction("StopAttack", &Script_StopAttack);
|
||||
Game::Lua::RegisterGlobalFunction("StartAttack", &Script_StartAttack, &kStartAttack);
|
||||
Game::Lua::RegisterGlobalFunction("StopAttack", &Script_StopAttack, &kStopAttack);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
+21
-9
@@ -72,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);
|
||||
@@ -410,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;
|
||||
@@ -477,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);
|
||||
@@ -497,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};
|
||||
|
||||
+47
-33
@@ -15,8 +15,8 @@
|
||||
// `PLAYER_SWING_RANGE_UPDATE(swingType, isInRange, checksRange)`.
|
||||
//
|
||||
// RANGED reuses the engine's generic range core: `Spell::Range::PlayerVsUnit`
|
||||
// wraps `FUN_SPELL_RANGE_CHECK` -> `FUN_006e3480`, feeding it Auto Shot/Shoot
|
||||
// as a pure geometry probe (it doesn't check spell knowledge). That core
|
||||
// wraps `FUN_SPELL_RANGE_CHECK` -> `FUN_006e3480`, feeding it the ranged
|
||||
// attack spell for the equipped weapon as a pure geometry probe (it doesn't check spell knowledge). That core
|
||||
// computes full 3D (X/Y/Z) distance, which matches the SERVER's generic
|
||||
// spell-range gate (`Spell::CheckRange`'s non-combat-range path ->
|
||||
// `WorldObject::GetCombatDistance`, also 3D) closely enough for ranged.
|
||||
@@ -47,10 +47,9 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "event/Custom.h"
|
||||
#include "item/CGItem.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
#include "spell/Known.h"
|
||||
#include "spell/Range.h"
|
||||
#include "tick/WorldTick.h"
|
||||
#include "unit/Position.h"
|
||||
@@ -70,36 +69,49 @@ bool g_enabled[3] = {false, false, false};
|
||||
RangeState g_lastState[3] = {RangeState::NoCheck, RangeState::NoCheck, RangeState::NoCheck};
|
||||
|
||||
using UnitCanAttack_t = bool(__thiscall *)(void *attacker, void *target);
|
||||
using EquippedItemCheck_t = char(__fastcall *)(void *caster, const uint8_t *spellRecord,
|
||||
char checkAmmo, char report);
|
||||
|
||||
// Pure geometry probe for Ranged -- the two ranged-weapon families.
|
||||
constexpr int kAutoShotSpellId = 75; // bow/gun/crossbow, RangeIndex 114 (8-35yd)
|
||||
constexpr int kShootSpellId = 5019; // wand, RangeIndex 4 (0-30yd)
|
||||
// The ranged attacks a player can make, as pure geometry probes. Spell.dbc
|
||||
// (EquippedItemSubClassMask, SpellRange row):
|
||||
// 75 Auto Shot bow, gun, thrown, crossbow 8-35 yd (hunters)
|
||||
// 5019 Shoot wand 0-30 yd
|
||||
// 2764 Throw thrown 8-30 yd
|
||||
// 2480 Shoot Bow bow 8-30 yd
|
||||
// 7918 Shoot Gun gun 8-30 yd
|
||||
// 7919 Shoot Crossbow crossbow 8-30 yd
|
||||
// Auto Shot comes first: it covers the weapons of Throw and the three
|
||||
// Shoots, and a hunter who knows it attacks with it at its longer range.
|
||||
// The other masks are disjoint.
|
||||
constexpr int kRangedAttackSpells[] = {Offsets::AUTO_SHOT_SPELL_ID, 5019, 2764,
|
||||
2480, 7918, 7919};
|
||||
|
||||
// While actively auto-shooting, the exact spell in flight is the truest
|
||||
// probe; otherwise pick by the equipped ranged weapon's ammo type (bows/
|
||||
// guns/crossbows consume ammo, wands don't -- the same distinction the
|
||||
// server itself uses to route SPELL_ATTR_EX2_AUTOREPEAT_FLAG casts).
|
||||
int RangedProbeSpellId() {
|
||||
// While auto-repeating, the spell in flight is the truest probe. Otherwise
|
||||
// the first spell the player knows whose weapon requirement the engine's
|
||||
// own equipped-item check passes, else the first one it passes (a range
|
||||
// for the weapon even before its attack is trained). 0 when none passes:
|
||||
// a broken ranged weapon cannot attack.
|
||||
int RangedProbeSpellId(void *player) {
|
||||
const auto active = Game::Read<uint32_t>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_ACTIVE_AUTO_REPEAT_SPELL));
|
||||
if (active != 0)
|
||||
return static_cast<int>(active);
|
||||
|
||||
const uint8_t *cgItem = Item::Location::ResolveEquipmentSlot(18); // ranged slot
|
||||
if (cgItem != nullptr) {
|
||||
const uint8_t *instance = Item::InstanceBlock(cgItem);
|
||||
if (instance != nullptr) {
|
||||
const uint32_t itemID = *reinterpret_cast<const uint32_t *>(
|
||||
instance + Offsets::OFF_INSTANCE_BLOCK_ITEM_ID);
|
||||
const uint8_t *stats = Item::PeekRecord(itemID);
|
||||
if (stats != nullptr) {
|
||||
const uint32_t ammoType = *reinterpret_cast<const uint32_t *>(
|
||||
stats + Offsets::OFF_ITEMSTATS_AMMO_TYPE);
|
||||
return ammoType != 0 ? kAutoShotSpellId : kShootSpellId;
|
||||
}
|
||||
}
|
||||
auto equipped =
|
||||
reinterpret_cast<EquippedItemCheck_t>(Offsets::FUN_SPELL_EQUIPPED_ITEM_CHECK);
|
||||
int fallback = 0;
|
||||
for (const int spellID : kRangedAttackSpells) {
|
||||
const uint8_t *rec = DBC::Record(Offsets::VAR_SPELL_RECORDS,
|
||||
Offsets::VAR_SPELL_RECORD_COUNT,
|
||||
static_cast<uint32_t>(spellID));
|
||||
if (rec == nullptr || !equipped(player, rec, 0, 0))
|
||||
continue;
|
||||
if (Spell::Known::IsKnown(static_cast<uint32_t>(spellID)))
|
||||
return spellID;
|
||||
if (fallback == 0)
|
||||
fallback = spellID;
|
||||
}
|
||||
return kAutoShotSpellId;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
RangeState Evaluate(SwingType type) {
|
||||
@@ -119,7 +131,10 @@ RangeState Evaluate(SwingType type) {
|
||||
return Combat::Swing::InMeleeRange(player, target) ? RangeState::InRange
|
||||
: RangeState::OutOfRange;
|
||||
|
||||
const int result = Spell::Range::PlayerVsUnit(RangedProbeSpellId(), "target");
|
||||
const int probe = RangedProbeSpellId(player);
|
||||
if (probe == 0)
|
||||
return RangeState::NoCheck;
|
||||
const int result = Spell::Range::PlayerVsUnit(probe, "target");
|
||||
if (result < 0)
|
||||
return RangeState::NoCheck;
|
||||
return result != 0 ? RangeState::InRange : RangeState::OutOfRange;
|
||||
@@ -226,8 +241,8 @@ const Game::Doc::Field kEnableArgs[] = {
|
||||
"needs the event."),
|
||||
};
|
||||
const Game::Doc::Function kEnableDoc{
|
||||
"Used with PLAYER_SWING_RANGE_UPDATE to be informed when the current target "
|
||||
"enters or leaves range for a swing type.",
|
||||
"Turns PLAYER_SWING_RANGE_UPDATE on or off for a swing type, which fires when the "
|
||||
"current target enters or leaves range for it.",
|
||||
kEnableArgs};
|
||||
|
||||
const Game::Doc::Field kIsInRangeArgs[] = {
|
||||
@@ -241,9 +256,8 @@ const Game::Doc::Field kIsInRangeRets[] = {
|
||||
"of range."),
|
||||
};
|
||||
const Game::Doc::Function kIsInRangeDoc{
|
||||
"Whether the current target is within range of the player's auto attack for "
|
||||
"the given swing type. Auto attacks only ever apply to the current target, "
|
||||
"so no other unit can be queried.",
|
||||
"Returns whether the current target is within range of the player's auto attack "
|
||||
"for the given swing type; only the current target can be checked.",
|
||||
kIsInRangeArgs, kIsInRangeRets};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
|
||||
+2
-14
@@ -45,6 +45,7 @@
|
||||
#include "time/Clock.h"
|
||||
#include "unit/Focus.h"
|
||||
#include "unit/Identity.h"
|
||||
#include "unit/Power.h"
|
||||
|
||||
#include <ctime>
|
||||
|
||||
@@ -617,17 +618,6 @@ uint32_t SpellSchoolMask(const uint8_t *spell) {
|
||||
: 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;
|
||||
}
|
||||
|
||||
Entry Begin(uint64_t source, uint64_t dest, int event, const uint8_t *spell) {
|
||||
Entry e;
|
||||
e.event = event;
|
||||
@@ -684,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -167,10 +167,6 @@ uint32_t SchoolMask(uint32_t school);
|
||||
// The spell's school as a mask (4.3.4's SpellRec m_schoolMask), 0 for nullptr.
|
||||
uint32_t SpellSchoolMask(const uint8_t *spell);
|
||||
|
||||
// Whether one of the spell's three effects applies the aura `auraName`
|
||||
// (EffectApplyAuraName).
|
||||
bool HasAuraEffect(const uint8_t *spell, int auraName);
|
||||
|
||||
// GetCombatLogEntry: an entry for `event` from `source` to `dest` (either may
|
||||
// be 0), with the spell prefix when `spell` is a record.
|
||||
Entry Begin(uint64_t source, uint64_t dest, int event, const uint8_t *spell);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -98,6 +98,24 @@ int __fastcall Script_GetAllCommands(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kConsoleCommandInfoFields[] = {
|
||||
Game::Doc::Req("command", "cstring", "The name typed in the console."),
|
||||
Game::Doc::Req("help", "cstring", "Empty when the command has no description."),
|
||||
Game::Doc::Req("category", "ConsoleCategory", "Always Debug through Gm."),
|
||||
Game::Doc::Req("commandType", "ConsoleCommandType", "Always Cvar or Command."),
|
||||
Game::Doc::Req("scriptContents", "cstring", "Always empty."),
|
||||
Game::Doc::Req("scriptParameters", "cstring", "Always empty."),
|
||||
};
|
||||
const Game::Doc::Structure kConsoleCommandInfo{"ConsoleCommandInfo", "ConsoleGlobals",
|
||||
kConsoleCommandInfoFields};
|
||||
|
||||
const Game::Doc::Field kGetAllCommandsRets[] = {
|
||||
Game::Doc::Req("commands", "table", "Array of ConsoleCommandInfo."),
|
||||
};
|
||||
const Game::Doc::Function kGetAllCommands{
|
||||
"Returns every console command and console variable the client has.", {},
|
||||
kGetAllCommandsRets, "ConsoleGlobals"};
|
||||
|
||||
// Both enums are Blizzard's, stated in full rather than derived from the
|
||||
// engine's own category table. They are an API contract, not a description of
|
||||
// what this client happens to have: an addon asking for a value we can never
|
||||
@@ -125,6 +143,15 @@ int __fastcall Script_ConsolePrintAllMatchingCommands(void *L) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kPrintAllMatchingCommandsArgs[] = {
|
||||
Game::Doc::Req("partialCommandText", "cstring",
|
||||
"The start of a command name, matched without regard to case; an empty "
|
||||
"string prints nothing."),
|
||||
};
|
||||
const Game::Doc::Function kPrintAllMatchingCommands{
|
||||
"Writes every console command whose name begins with the given text to the console.",
|
||||
kPrintAllMatchingCommandsArgs, {}, "ConsoleGlobals"};
|
||||
|
||||
const Game::Lua::EnumIntegerEntry kCommandTypeEntries[] = {
|
||||
{"Cvar", kCommandTypeCvar},
|
||||
{"Command", kCommandTypeCommand},
|
||||
@@ -142,15 +169,14 @@ const Game::Lua::EnumIntegerEntry kCategoryEntries[] = {
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("ConsoleGetAllCommands", &Script_GetAllCommands);
|
||||
Game::Lua::RegisterGlobalFunction("ConsoleGetAllCommands", &Script_GetAllCommands,
|
||||
&kGetAllCommands);
|
||||
Game::Lua::RegisterGlobalFunction("ConsolePrintAllMatchingCommands",
|
||||
&Script_ConsolePrintAllMatchingCommands);
|
||||
&Script_ConsolePrintAllMatchingCommands,
|
||||
&kPrintAllMatchingCommands);
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "ConsoleCommandType", kCommandTypeEntries,
|
||||
sizeof(kCommandTypeEntries) /
|
||||
sizeof(kCommandTypeEntries[0]));
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "ConsoleCategory", kCategoryEntries,
|
||||
sizeof(kCategoryEntries) /
|
||||
sizeof(kCategoryEntries[0]));
|
||||
"ConsoleGlobals");
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "ConsoleCategory", kCategoryEntries, "ConsoleGlobals");
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
+82
-14
@@ -59,6 +59,13 @@ int __fastcall Script_ConsoleExec(void *L) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kConsoleExecArgs[] = {
|
||||
Game::Doc::Req("command", "cstring", "A command line, as typed into the console."),
|
||||
};
|
||||
const Game::Doc::Function kConsoleExec{
|
||||
"Runs a console command line, the same way typing it into the console does.",
|
||||
kConsoleExecArgs, {}, "ConsoleGlobals"};
|
||||
|
||||
// `ConsoleEcho(message [, colorType])` — write one line to the console.
|
||||
//
|
||||
// The engine no-ops cleanly when the graphics device is not up, so this is safe
|
||||
@@ -78,6 +85,14 @@ int __fastcall Script_ConsoleEcho(void *L) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kConsoleEchoArgs[] = {
|
||||
Game::Doc::Req("message", "cstring", "The text to write."),
|
||||
Game::Doc::Opt("colorType", "number", "0",
|
||||
"The line colour, 0 to 8 as ConsoleGetColorFromType takes; other values use 0."),
|
||||
};
|
||||
const Game::Doc::Function kConsoleEcho{"Writes one line to the console in a chosen colour.",
|
||||
kConsoleEchoArgs, {}, "ConsoleGlobals"};
|
||||
|
||||
// `ConsoleAddMessage(message)` — 4.3.4's Script_ConsoleAddMessage
|
||||
// (0x008C38B0): a usage error for a non-string, else ConsolePrintf("%s")
|
||||
// unless the text is empty.
|
||||
@@ -110,6 +125,13 @@ int __fastcall Script_ConsoleIsActive(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kConsoleIsActiveRets[] = {
|
||||
Game::Doc::Req("consoleIsActive", "bool",
|
||||
"Always false when the client was started without -console."),
|
||||
};
|
||||
const Game::Doc::Function kConsoleIsActive{"Returns whether the console is open.", {},
|
||||
kConsoleIsActiveRets, "ConsoleGlobals"};
|
||||
|
||||
// `ConsoleGetColorFromType(colorType)` -> r, g, b, a
|
||||
//
|
||||
// The colour the console draws that type of line in, as four 0..1 components to
|
||||
@@ -133,6 +155,20 @@ int __fastcall Script_ConsoleGetColorFromType(void *L) {
|
||||
return 4;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kConsoleGetColorFromTypeArgs[] = {
|
||||
Game::Doc::Req("colorType", "number", "From 0 to 8."),
|
||||
};
|
||||
const Game::Doc::Field kConsoleGetColorFromTypeRets[] = {
|
||||
Game::Doc::Opt("r", "number", nullptr,
|
||||
"From 0 to 1; nil, with the rest, when colorType is not 0 to 8."),
|
||||
Game::Doc::Opt("g", "number", nullptr, "From 0 to 1."),
|
||||
Game::Doc::Opt("b", "number", nullptr, "From 0 to 1."),
|
||||
Game::Doc::Opt("a", "number", nullptr, "From 0 to 1."),
|
||||
};
|
||||
const Game::Doc::Function kConsoleGetColorFromType{
|
||||
"Returns the colour the console draws one type of line in.",
|
||||
kConsoleGetColorFromTypeArgs, kConsoleGetColorFromTypeRets, "ConsoleGlobals"};
|
||||
|
||||
// `ConsoleGetFontHeight()` — the console's text height, as a fraction of the
|
||||
// screen height.
|
||||
int __fastcall Script_ConsoleGetFontHeight(void *L) {
|
||||
@@ -140,6 +176,12 @@ int __fastcall Script_ConsoleGetFontHeight(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kConsoleGetFontHeightRets[] = {
|
||||
Game::Doc::Req("fontHeight", "number", "A fraction of the screen height, not pixels."),
|
||||
};
|
||||
const Game::Doc::Function kConsoleGetFontHeight{"Returns the height of the console's text.", {},
|
||||
kConsoleGetFontHeightRets, "ConsoleGlobals"};
|
||||
|
||||
// There is deliberately no ConsoleSetFontHeight. Writing the height global
|
||||
// alone changes nothing, since the font was already built at the old size, and
|
||||
// the only thing that rebuilds it is the console's font init — which has no
|
||||
@@ -166,6 +208,12 @@ int __fastcall Script_SetConsoleKey(void *L) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kSetConsoleKeyArgs[] = {
|
||||
Game::Doc::Req("keyCode", "number", "The client's numeric key code, not a key name."),
|
||||
};
|
||||
const Game::Doc::Function kSetConsoleKey{"Sets the key that opens and closes the console.",
|
||||
kSetConsoleKeyArgs, {}, "ConsoleGlobals"};
|
||||
|
||||
// `CalculateStringEditDistance(a, b)` — Levenshtein distance, the number of
|
||||
// single-character insertions, deletions or substitutions between two strings.
|
||||
// The console uses it to suggest a command when one is mistyped.
|
||||
@@ -226,32 +274,52 @@ int __fastcall Script_CalculateStringEditDistance(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kCalculateStringEditDistanceArgs[] = {
|
||||
Game::Doc::Req("firstString", "string"),
|
||||
Game::Doc::Req("secondString", "string"),
|
||||
};
|
||||
const Game::Doc::Field kCalculateStringEditDistanceRets[] = {
|
||||
Game::Doc::Opt("distance", "number", nullptr,
|
||||
"Nil when an argument is not a string, or the shorter string is 256 "
|
||||
"characters or longer."),
|
||||
};
|
||||
const Game::Doc::Function kCalculateStringEditDistance{
|
||||
"Returns how many single-character insertions, deletions or substitutions turn one "
|
||||
"string into the other.",
|
||||
kCalculateStringEditDistanceArgs, kCalculateStringEditDistanceRets, "ConsoleGlobals"};
|
||||
|
||||
void Register() {
|
||||
Game::Lua::RegisterGlobalFunction("ConsoleExec", &Script_ConsoleExec);
|
||||
Game::Lua::RegisterGlobalFunction("ConsoleEcho", &Script_ConsoleEcho);
|
||||
Game::Lua::RegisterGlobalFunction("ConsoleExec", &Script_ConsoleExec, &kConsoleExec);
|
||||
Game::Lua::RegisterGlobalFunction("ConsoleEcho", &Script_ConsoleEcho, &kConsoleEcho);
|
||||
Game::Lua::RegisterGlobalFunction("ConsoleAddMessage", &Script_ConsoleAddMessage,
|
||||
&kConsoleAddMessage);
|
||||
Game::Lua::RegisterGlobalFunction("ConsoleIsActive", &Script_ConsoleIsActive);
|
||||
Game::Lua::RegisterGlobalFunction("ConsoleIsActive", &Script_ConsoleIsActive,
|
||||
&kConsoleIsActive);
|
||||
Game::Lua::RegisterGlobalFunction("ConsoleGetColorFromType",
|
||||
&Script_ConsoleGetColorFromType);
|
||||
Game::Lua::RegisterGlobalFunction("ConsoleGetFontHeight", &Script_ConsoleGetFontHeight);
|
||||
Game::Lua::RegisterGlobalFunction("SetConsoleKey", &Script_SetConsoleKey);
|
||||
&Script_ConsoleGetColorFromType, &kConsoleGetColorFromType);
|
||||
Game::Lua::RegisterGlobalFunction("ConsoleGetFontHeight", &Script_ConsoleGetFontHeight,
|
||||
&kConsoleGetFontHeight);
|
||||
Game::Lua::RegisterGlobalFunction("SetConsoleKey", &Script_SetConsoleKey, &kSetConsoleKey);
|
||||
Game::Lua::RegisterGlobalFunction("CalculateStringEditDistance",
|
||||
&Script_CalculateStringEditDistance);
|
||||
&Script_CalculateStringEditDistance,
|
||||
&kCalculateStringEditDistance);
|
||||
}
|
||||
|
||||
// The console exists before login, so the same surface is registered on the
|
||||
// glue state.
|
||||
void RegisterGlue() {
|
||||
Game::Lua::RegisterGlueFunction("ConsoleExec", &Script_ConsoleExec);
|
||||
Game::Lua::RegisterGlueFunction("ConsoleEcho", &Script_ConsoleEcho);
|
||||
Game::Lua::RegisterGlueFunction("ConsoleIsActive", &Script_ConsoleIsActive);
|
||||
Game::Lua::RegisterGlueFunction("ConsoleExec", &Script_ConsoleExec, &kConsoleExec);
|
||||
Game::Lua::RegisterGlueFunction("ConsoleEcho", &Script_ConsoleEcho, &kConsoleEcho);
|
||||
Game::Lua::RegisterGlueFunction("ConsoleIsActive", &Script_ConsoleIsActive,
|
||||
&kConsoleIsActive);
|
||||
Game::Lua::RegisterGlueFunction("ConsoleGetColorFromType",
|
||||
&Script_ConsoleGetColorFromType);
|
||||
Game::Lua::RegisterGlueFunction("ConsoleGetFontHeight", &Script_ConsoleGetFontHeight);
|
||||
Game::Lua::RegisterGlueFunction("SetConsoleKey", &Script_SetConsoleKey);
|
||||
&Script_ConsoleGetColorFromType, &kConsoleGetColorFromType);
|
||||
Game::Lua::RegisterGlueFunction("ConsoleGetFontHeight", &Script_ConsoleGetFontHeight,
|
||||
&kConsoleGetFontHeight);
|
||||
Game::Lua::RegisterGlueFunction("SetConsoleKey", &Script_SetConsoleKey, &kSetConsoleKey);
|
||||
Game::Lua::RegisterGlueFunction("CalculateStringEditDistance",
|
||||
&Script_CalculateStringEditDistance);
|
||||
&Script_CalculateStringEditDistance,
|
||||
&kCalculateStringEditDistance);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Register};
|
||||
|
||||
@@ -62,9 +62,23 @@ 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);
|
||||
&Script_C_Container_AutoStoreItem, &kAutoStoreItem);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "equipmentset/Locations.h"
|
||||
#include "item/BagFamily.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
@@ -47,7 +46,7 @@ bool ViewFor(int bagID, View *out) {
|
||||
return PlayerRange(Offsets::INVMGR_BACKPACK_FIRST_SLOT,
|
||||
Offsets::BACKPACK_NUM_SLOTS, out);
|
||||
if (bagID == Offsets::BAG_INDEX_BANK) {
|
||||
if (!EquipmentSet::Locations::BankerOpen())
|
||||
if (!Item::Location::BankerOpen())
|
||||
return false;
|
||||
return PlayerRange(Offsets::INVMGR_BANK_MAIN_FIRST_SLOT,
|
||||
Offsets::INVMGR_BANK_MAIN_LAST_SLOT -
|
||||
|
||||
@@ -42,8 +42,7 @@ const Game::Lua::EnumIntegerEntry kEntries[] = {
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "BagIndex", kEntries,
|
||||
sizeof(kEntries) / sizeof(kEntries[0]), "BagIndexConstants");
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "BagIndex", kEntries, "BagIndexConstants");
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -43,9 +43,21 @@ 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);
|
||||
&Script_C_Container_GetContainerItemCharges,
|
||||
&kGetContainerItemCharges);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -33,9 +33,22 @@ 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);
|
||||
&Script_C_Container_GetItemCooldown, &kGetItemCooldown);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -40,9 +40,22 @@ 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);
|
||||
&Script_C_Container_GetContainerItemDurability,
|
||||
&kGetContainerItemDurability);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -80,10 +80,25 @@ 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);
|
||||
&Script_C_Container_GetContainerItemEquipmentSetInfo,
|
||||
&kGetContainerItemEquipmentSetInfo);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -96,15 +96,45 @@ int __fastcall Script_C_Container_CalculateTotalNumberOfFreeBagSlots(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kBagArgs[] = {
|
||||
Game::Doc::Req("bagIndex", "BagIndex", "0 for the backpack, 1 to 4 for the equipped bags."),
|
||||
};
|
||||
const Game::Doc::Field kNumFreeSlotsRets[] = {
|
||||
Game::Doc::Req("numFreeSlots", "number", "0 when the bag does not exist."),
|
||||
Game::Doc::Req("bagFamily", "number",
|
||||
"The bag's family bitmask; 0 for a bag that takes any item."),
|
||||
};
|
||||
const Game::Doc::Function kGetContainerNumFreeSlots{
|
||||
"Returns the number of empty slots in a bag and the kind of items the bag takes.",
|
||||
kBagArgs, kNumFreeSlotsRets};
|
||||
|
||||
const Game::Doc::Field kFreeSlotsRets[] = {
|
||||
Game::Doc::Opt("freeSlots", "table", nullptr,
|
||||
"The empty slot numbers in ascending order; nil when the bag does not exist."),
|
||||
};
|
||||
const Game::Doc::Function kGetContainerFreeSlots{
|
||||
"Returns the numbers of the empty slots in a bag.", kBagArgs, kFreeSlotsRets};
|
||||
|
||||
const Game::Doc::Field kTotalRets[] = {
|
||||
Game::Doc::Req("totalFreeSlots", "number"),
|
||||
};
|
||||
const Game::Doc::Function kCalculateTotalNumberOfFreeBagSlots{
|
||||
"Returns the number of empty slots in the backpack and the equipped bags that take "
|
||||
"any item.",
|
||||
{}, kTotalRets};
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetContainerNumFreeSlots",
|
||||
&Script_C_Container_GetContainerNumFreeSlots);
|
||||
&Script_C_Container_GetContainerNumFreeSlots,
|
||||
&kGetContainerNumFreeSlots);
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetContainerFreeSlots",
|
||||
&Script_C_Container_GetContainerFreeSlots);
|
||||
&Script_C_Container_GetContainerFreeSlots,
|
||||
&kGetContainerFreeSlots);
|
||||
Game::Lua::RegisterTableFunction("C_Container", "CalculateTotalNumberOfFreeBagSlots",
|
||||
&Script_C_Container_CalculateTotalNumberOfFreeBagSlots);
|
||||
&Script_C_Container_CalculateTotalNumberOfFreeBagSlots,
|
||||
&kCalculateTotalNumberOfFreeBagSlots);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -40,9 +40,20 @@ int __fastcall Script_C_Container_HasContainerItem(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("containerIndex", "BagIndex"),
|
||||
Game::Doc::Req("slotIndex", "luaIndex"),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Req("hasItem", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kHasContainerItem{
|
||||
"Returns whether a bag slot holds an item, even one whose data is not cached yet.",
|
||||
kArgs, kRets};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "HasContainerItem",
|
||||
&Script_C_Container_HasContainerItem);
|
||||
&Script_C_Container_HasContainerItem, &kHasContainerItem);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -117,13 +117,27 @@ int __fastcall Script_C_Container_UseHearthstone(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kHasRets[] = {
|
||||
Game::Doc::Opt("itemID", "number", nullptr, "Nil when no hearthstone is in the bags."),
|
||||
};
|
||||
const Game::Doc::Function kPlayerHasHearthstone{
|
||||
"Returns the ID of the hearthstone in the player's bags.", {}, kHasRets};
|
||||
|
||||
const Game::Doc::Field kUseRets[] = {
|
||||
Game::Doc::Req("used", "bool",
|
||||
"True when a hearthstone was found; the cast itself can still fail."),
|
||||
};
|
||||
const Game::Doc::Function kUseHearthstone{"Uses the hearthstone in the player's bags.", {},
|
||||
kUseRets};
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "PlayerHasHearthstone",
|
||||
&Script_C_Container_PlayerHasHearthstone);
|
||||
&Script_C_Container_PlayerHasHearthstone,
|
||||
&kPlayerHasHearthstone);
|
||||
Game::Lua::RegisterTableFunction("C_Container", "UseHearthstone",
|
||||
&Script_C_Container_UseHearthstone);
|
||||
&Script_C_Container_UseHearthstone, &kUseHearthstone);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
+11
-1
@@ -41,9 +41,19 @@ int __fastcall Script_C_Container_GetContainerItemID(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("containerIndex", "BagIndex"),
|
||||
Game::Doc::Req("slotIndex", "luaIndex"),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Opt("itemID", "number", nullptr, "Nil when the slot is empty."),
|
||||
};
|
||||
const Game::Doc::Function kGetContainerItemID{"Returns the ID of the item in a bag slot.",
|
||||
kArgs, kRets};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemID",
|
||||
&Script_C_Container_GetContainerItemID);
|
||||
&Script_C_Container_GetContainerItemID, &kGetContainerItemID);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -217,7 +217,7 @@ const Game::Doc::Field kArgs[] = {
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Opt("containerInfo", "ContainerItemInfo", nullptr, "Nil when the slot is empty."),
|
||||
};
|
||||
const Game::Doc::Function kGetContainerItemInfo{"The item in a bag slot.", kArgs, kRets};
|
||||
const Game::Doc::Function kGetContainerItemInfo{"Returns the item in a bag slot.", kArgs, kRets};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemInfo",
|
||||
|
||||
@@ -57,9 +57,23 @@ int __fastcall Script_C_Container_MoveItem(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("srcBag", "BagIndex"),
|
||||
Game::Doc::Req("srcSlot", "luaIndex"),
|
||||
Game::Doc::Req("dstBag", "BagIndex"),
|
||||
Game::Doc::Req("dstSlot", "luaIndex"),
|
||||
Game::Doc::Req("count", "number", "From 1 to 255."),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Req("sent", "bool", "False when an argument is not valid."),
|
||||
};
|
||||
const Game::Doc::Function kMoveItem{
|
||||
"Moves part of a stack to another bag slot without the cursor.", kArgs, kRets, nullptr,
|
||||
true};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "MoveItem",
|
||||
&Script_C_Container_MoveItem);
|
||||
Game::Lua::RegisterTableFunction("C_Container", "MoveItem", &Script_C_Container_MoveItem,
|
||||
&kMoveItem);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -37,9 +37,24 @@ int __fastcall Script_C_Container_IsContainerItemOpenable(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("isOpenable", "bool", nullptr,
|
||||
"Nil when the slot is empty or the item's data is not cached yet."),
|
||||
Game::Doc::Opt("canOpen", "bool", nullptr, "False while the item is still locked."),
|
||||
};
|
||||
const Game::Doc::Function kIsContainerItemOpenable{
|
||||
"Returns whether the item in a bag slot can be opened, and whether the player can "
|
||||
"open it now.",
|
||||
kArgs, kRets, nullptr, true};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "IsContainerItemOpenable",
|
||||
&Script_C_Container_IsContainerItemOpenable);
|
||||
&Script_C_Container_IsContainerItemOpenable,
|
||||
&kIsContainerItemOpenable);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -97,9 +97,29 @@ int __fastcall Script_C_Container_GetContainerItemQuestInfo(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kItemQuestInfoFields[] = {
|
||||
Game::Doc::Req("isQuestItem", "bool"),
|
||||
Game::Doc::Opt("questID", "number", nullptr,
|
||||
"The quest the item begins; nil when it begins none."),
|
||||
Game::Doc::Req("isActive", "bool", "True when the quest in questID is in the quest log."),
|
||||
};
|
||||
const Game::Doc::Structure kItemQuestInfo{"ItemQuestInfo", "Container", kItemQuestInfoFields};
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("containerIndex", "BagIndex"),
|
||||
Game::Doc::Req("slotIndex", "luaIndex"),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Req("questInfo", "ItemQuestInfo",
|
||||
"Always a table; an empty slot reports no quest."),
|
||||
};
|
||||
const Game::Doc::Function kGetContainerItemQuestInfo{
|
||||
"Returns how the item in a bag slot relates to quests.", kArgs, kRets};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemQuestInfo",
|
||||
&Script_C_Container_GetContainerItemQuestInfo);
|
||||
&Script_C_Container_GetContainerItemQuestInfo,
|
||||
&kGetContainerItemQuestInfo);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -37,9 +37,21 @@ int __fastcall Script_C_Container_GetContainerItemRepairCost(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::Req("repairCost", "number", "In copper; 0 when the item needs no repair."),
|
||||
};
|
||||
const Game::Doc::Function kGetContainerItemRepairCost{
|
||||
"Returns what a merchant charges to repair the item in a bag slot.", kArgs, kRets,
|
||||
nullptr, true};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemRepairCost",
|
||||
&Script_C_Container_GetContainerItemRepairCost);
|
||||
&Script_C_Container_GetContainerItemRepairCost,
|
||||
&kGetContainerItemRepairCost);
|
||||
}
|
||||
|
||||
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