mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-11 20:37:37 +00:00
Compare commits
133
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4477749bd6 | ||
|
|
99fde6e1bb | ||
|
|
ea7e215a0c | ||
|
|
f5b240de77 | ||
|
|
bb5acbbdfb | ||
|
|
0825491af2 | ||
|
|
2a4731f9e5 | ||
|
|
0d639a0a35 | ||
|
|
8dc7cf8ace | ||
|
|
1e0871b639 | ||
|
|
70a6b99bfe | ||
|
|
edd7fb8577 | ||
|
|
34b061e3da | ||
|
|
ddffa12f2f | ||
|
|
d7ff5d8a5c | ||
|
|
3df06c056d | ||
|
|
67e833557a | ||
|
|
5a78289124 | ||
|
|
9f166b6770 | ||
|
|
6b4224d281 | ||
|
|
79d4571f8b | ||
|
|
8982ec757b | ||
|
|
b44b0d2f59 | ||
|
|
76eb5d9135 | ||
|
|
aa88e776e9 | ||
|
|
d74952c9ac | ||
|
|
2567ac1bd8 | ||
|
|
d5f93a1e7f | ||
|
|
3132eb80d8 | ||
|
|
cf6593aa7b | ||
|
|
0cb101ef97 | ||
|
|
545cc70a9d | ||
|
|
2fda1cdbcd | ||
|
|
13ccfb0865 | ||
|
|
7fa9ad9787 | ||
|
|
2cb0e4b5b7 | ||
|
|
9f77a40235 | ||
|
|
76c3cd3a37 | ||
|
|
7f05e88eaf | ||
|
|
229a5d51ae | ||
|
|
12eeac5050 | ||
|
|
f5f9485375 | ||
|
|
aaa033bb4f | ||
|
|
39a774930b | ||
|
|
5ee83e5427 | ||
|
|
3791981873 | ||
|
|
8f537226e9 | ||
|
|
6532eb76eb | ||
|
|
098432bee1 | ||
|
|
220fbff57a | ||
|
|
bddd6d91fa | ||
|
|
4a72ca1138 | ||
|
|
75653b9485 | ||
|
|
dfa29376f2 | ||
|
|
7be139fe32 | ||
|
|
c61e3b6c52 | ||
|
|
5783814dc5 | ||
|
|
afd5127f50 | ||
|
|
cfa4645cad | ||
|
|
8640cc02da | ||
|
|
bc32b9b645 | ||
|
|
5bb14c036e | ||
|
|
e5227157b9 | ||
|
|
be6a3c5bef | ||
|
|
4e8dc39e65 | ||
|
|
d53e15ff45 | ||
|
|
5c5bedece6 | ||
|
|
a258c856d1 | ||
|
|
e9340786c9 | ||
|
|
9101de7f78 | ||
|
|
8b6b6ce4ce | ||
|
|
ea6b3714ed | ||
|
|
2e87d286ff | ||
|
|
03afa419f2 | ||
|
|
5d5682cbdd | ||
|
|
69665cdadf | ||
|
|
81d63bc11c | ||
|
|
ce87b64ab2 | ||
|
|
af01cda9b6 | ||
|
|
a878407ed6 | ||
|
|
8ed9c0f707 | ||
|
|
06c9594e2a | ||
|
|
83298e03d7 | ||
|
|
849b9b2c92 | ||
|
|
e27d1e5849 | ||
|
|
2ba1818a6e | ||
|
|
8ec08a43b1 | ||
|
|
82756a9d25 | ||
|
|
38771147d0 | ||
|
|
ce1ac37964 | ||
|
|
1c70be0e3d | ||
|
|
56d618b1d4 | ||
|
|
ac09c59d90 | ||
|
|
b6fa913732 | ||
|
|
cf76b699a2 | ||
|
|
dc394ab046 | ||
|
|
12b3784333 | ||
|
|
f6b37d5fa1 | ||
|
|
754997ea51 | ||
|
|
a16b99073a | ||
|
|
d7103de564 | ||
|
|
c00eb24a8d | ||
|
|
b65c9e0319 | ||
|
|
de033896b1 | ||
|
|
dc8d4caf12 | ||
|
|
20069f3815 | ||
|
|
f2fa112f0a | ||
|
|
fca967c827 | ||
|
|
24404b5a23 | ||
|
|
a106d86969 | ||
|
|
fac3be3a0b | ||
|
|
b49c107778 | ||
|
|
53b7f79440 | ||
|
|
7b21453469 | ||
|
|
ebfd9d034d | ||
|
|
d825bcbdda | ||
|
|
6b36bb10e6 | ||
|
|
6628dcca19 | ||
|
|
55d94c80d6 | ||
|
|
c0b403ba05 | ||
|
|
f6b98bc3ec | ||
|
|
8f4f574174 | ||
|
|
43a989dd56 | ||
|
|
0caddb8fe6 | ||
|
|
8e603a7a0b | ||
|
|
4726fd7c85 | ||
|
|
3e0c40184b | ||
|
|
b3e8d92b03 | ||
|
|
0a3f8324ea | ||
|
|
6f3a8ffc1d | ||
|
|
3862408e1a | ||
|
|
12a81f7f89 | ||
|
|
05fa5845b7 |
@@ -10,3 +10,4 @@
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.ico binary
|
||||
*.blp binary
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
-- 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 SPELLS = BW.SPELLS
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- The brood
|
||||
|
||||
-- Health grows faster as the run goes on (x3 at 5:00, x9 at 10:00), so late
|
||||
-- upgrades keep pace instead of running away; bites grow x4 by 10:00.
|
||||
local function KindStats(kind)
|
||||
local m = BW.run.time / 300;
|
||||
local grow = 1 + 2 * m + m * m;
|
||||
return math.floor(kind.hp * grow + 0.5), kind.dps * grow;
|
||||
end
|
||||
|
||||
local function NewEnemy(kind)
|
||||
local e = { kind = kind, actor = BW.scene:AcquireActor() };
|
||||
e.actor:SetModelByCreatureDisplayID(kind.display);
|
||||
-- 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
|
||||
|
||||
local function Spawn(kind)
|
||||
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
|
||||
local pool = pools[kind.key];
|
||||
local e = pool and table.remove(pool) or NewEnemy(kind);
|
||||
e.hp, e.dps = KindStats(kind);
|
||||
e.maxHp = e.hp;
|
||||
e.alive = true;
|
||||
e.dying = nil;
|
||||
e.fading = nil;
|
||||
e.dotLeft = nil;
|
||||
e.slowUntil, e.slowFactor = nil, nil;
|
||||
e.stunUntil = nil;
|
||||
e.fearUntil, e.fearTick = nil, nil;
|
||||
e.frozenUntil = nil;
|
||||
e.weakUntil, e.weakShare = nil, nil;
|
||||
e.taunter, e.tauntUntil = nil, nil;
|
||||
if e.kit then
|
||||
e.kit, e.kitUntil = nil, nil;
|
||||
e.actor:SetSpellVisualKit();
|
||||
end
|
||||
e.angle = math.random() * 2 * math.pi;
|
||||
local actor = e.actor;
|
||||
actor:SetPosition(x, y, FloorAt(x, y, CENTER[3]) + 0.5);
|
||||
actor:SetAnimation(ANIM_STAND);
|
||||
actor:Show();
|
||||
actor:SetMovementEnabled(true);
|
||||
actor:SetMovementSpeed("run", kind.speed);
|
||||
tinsert(enemies, e);
|
||||
BW.run.alive = BW.run.alive + 1;
|
||||
return e;
|
||||
end
|
||||
|
||||
local function Kill(e)
|
||||
e.alive = false;
|
||||
e.dying = DEATH_SECONDS;
|
||||
BW.run.alive = BW.run.alive - 1;
|
||||
BW.run.kills = BW.run.kills + 1;
|
||||
local x, y, z = e.actor:GetPosition();
|
||||
-- The corpse keeps its movement until it is released: it stops where
|
||||
-- it is, plays Death, then lies Dead, as a dead unit does.
|
||||
e.actor:StopMoving();
|
||||
e.actor:SetAnimation(ANIM_DEATH);
|
||||
if e.kit then
|
||||
e.kit, e.kitUntil = nil, nil;
|
||||
e.actor:SetSpellVisualKit();
|
||||
end
|
||||
e.fearUntil = nil;
|
||||
DropGem(x, y, z, e.kind.xp, e.kind.gemScale);
|
||||
BW.DropItem(e, x, y, z);
|
||||
-- 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;
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Hit feedback: a short flash on the model, and a number that rises from the
|
||||
-- hit point and fades.
|
||||
|
||||
local FLASH_SECONDS = 0.12
|
||||
local NUMBER_SECONDS = 0.8
|
||||
local NUMBER_RISE = 34 -- pixels over the number's life
|
||||
local NUMBER_FONT = "Fonts\\ARIALN.TTF"
|
||||
local BIG_HIT = 40 -- hits this large use the large size
|
||||
local MAX_NUMBERS = 80
|
||||
|
||||
local numbers = {} -- live numbers, oldest first
|
||||
local numberPool = {}
|
||||
|
||||
local function TakeNumber()
|
||||
local n = table.remove(numberPool);
|
||||
if n then
|
||||
return n;
|
||||
end
|
||||
if table.getn(numbers) >= MAX_NUMBERS then
|
||||
-- Too many at once: the oldest gives up its string.
|
||||
return table.remove(numbers, 1);
|
||||
end
|
||||
n = { text = BW.feedback:CreateFontString(nil, "OVERLAY") };
|
||||
n.text:SetFontObject(NumberFontNormal);
|
||||
return n;
|
||||
end
|
||||
|
||||
local function ShowNumber(e, amount, color, text)
|
||||
local x, y, z = e.actor:GetNamePosition();
|
||||
if not x then
|
||||
x, y, z = e.actor:GetPosition();
|
||||
z = z + 1.5;
|
||||
end
|
||||
local n = TakeNumber();
|
||||
n.x, n.y, n.z = x, y, z;
|
||||
n.age = 0;
|
||||
n.dx = math.random(-12, 12);
|
||||
local size = amount >= BIG_HIT and 22 or 16;
|
||||
if n.size ~= size then
|
||||
n.size = size;
|
||||
n.text:SetFont(NUMBER_FONT, size, "OUTLINE");
|
||||
end
|
||||
n.text:SetTextColor(color[1], color[2], color[3]);
|
||||
n.text:SetText(text or tostring(amount));
|
||||
n.text:SetAlpha(1);
|
||||
n.text:Show();
|
||||
tinsert(numbers, n);
|
||||
end
|
||||
|
||||
local function UpdateNumbers(dt)
|
||||
local count = table.getn(numbers);
|
||||
if count == 0 then
|
||||
return;
|
||||
end
|
||||
local scale = BW.scene:GetEffectiveScale();
|
||||
for i = count, 1, -1 do
|
||||
local n = numbers[i];
|
||||
n.age = n.age + dt;
|
||||
local sx, sy = BW.scene:Project3DPointTo2D(n.x, n.y, n.z);
|
||||
if n.age >= NUMBER_SECONDS or not sx then
|
||||
n.text:Hide();
|
||||
tinsert(numberPool, n);
|
||||
table.remove(numbers, i);
|
||||
else
|
||||
local t = n.age / NUMBER_SECONDS;
|
||||
n.text:ClearAllPoints();
|
||||
n.text:SetPoint("BOTTOM", BW.scene, "BOTTOMLEFT", sx / scale + n.dx, sy / scale + NUMBER_RISE * t);
|
||||
if t > 0.5 then
|
||||
n.text:SetAlpha(2 - 2 * t);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function ClearNumbers()
|
||||
for i = table.getn(numbers), 1, -1 do
|
||||
numbers[i].text:Hide();
|
||||
tinsert(numberPool, numbers[i]);
|
||||
numbers[i] = nil;
|
||||
end
|
||||
end
|
||||
|
||||
local function Flash(e)
|
||||
e.flash = FLASH_SECONDS;
|
||||
if not e.lit then
|
||||
e.lit = true;
|
||||
e.actor:SetHighlight("mouseover", true);
|
||||
end
|
||||
end
|
||||
|
||||
local function Unflash(e)
|
||||
if e.lit then
|
||||
e.lit = false;
|
||||
e.actor:SetHighlight("mouseover", false);
|
||||
end
|
||||
end
|
||||
|
||||
local function Damage(e, amount, color)
|
||||
if not e.alive then
|
||||
return;
|
||||
end
|
||||
-- Masteries: Corruption and Frost Nova's Shatter make their marks take more.
|
||||
if e.dotLeft and Mastered("corruption") then
|
||||
amount = amount * SPELLS.corruption.mastery.amplify;
|
||||
end
|
||||
if e.frozenUntil and BW.run.time < e.frozenUntil and Mastered("frostnova") then
|
||||
amount = amount * SPELLS.frostnova.mastery.amplify;
|
||||
end
|
||||
local dealt = math.floor(amount * BW.run.damageScale + 0.5);
|
||||
e.hp = e.hp - dealt;
|
||||
if BW.run.passives.vampiric then
|
||||
BW.run.leech = BW.run.leech + dealt * VAMPIRIC_SHARE * BW.run.passives.vampiric;
|
||||
end
|
||||
Flash(e);
|
||||
ShowNumber(e, dealt, color);
|
||||
if e.hp <= 0 then
|
||||
Kill(e);
|
||||
end
|
||||
end
|
||||
|
||||
-- Throws an enemy away from (x, y) at `knock` (out, up). A corpse flies too,
|
||||
-- in its death pose.
|
||||
local function Blast(e, x, y, knock)
|
||||
local share = e.kind == ELITE and TUNING.ELITE_KNOCK_SHARE or 1;
|
||||
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
|
||||
-- can show a SpellVisualKit on the enemy while it lasts. Elites shrug off
|
||||
-- part of a hold or a fear.
|
||||
|
||||
local function ShowKit(e, kit, untilTime)
|
||||
if not kit then
|
||||
return;
|
||||
end
|
||||
if e.kit ~= kit then
|
||||
e.kit = kit;
|
||||
e.actor:SetSpellVisualKit(kit);
|
||||
end
|
||||
e.kitUntil = math.max(e.kitUntil or 0, untilTime);
|
||||
end
|
||||
|
||||
local function HoldShare(e)
|
||||
return e.kind == ELITE and TUNING.ELITE_HOLD_SHARE or 1;
|
||||
end
|
||||
|
||||
-- `frozen`: the hold is ice (Frost Nova's Shatter counts it).
|
||||
local function Hold(e, seconds, kit, frozen)
|
||||
e.stunUntil = math.max(e.stunUntil or 0, BW.run.time + seconds * HoldShare(e));
|
||||
if frozen then
|
||||
e.frozenUntil = e.stunUntil;
|
||||
end
|
||||
e.fearUntil = nil;
|
||||
e.actor:StopMoving();
|
||||
ShowKit(e, kit, e.stunUntil);
|
||||
end
|
||||
|
||||
local function Fear(e, seconds, kit)
|
||||
e.fearUntil = math.max(e.fearUntil or 0, BW.run.time + seconds * HoldShare(e));
|
||||
ShowKit(e, kit, e.fearUntil);
|
||||
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
|
||||
|
||||
-- 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.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.color);
|
||||
if not e.alive then
|
||||
return;
|
||||
end
|
||||
end
|
||||
end
|
||||
if e.kitUntil and t >= e.kitUntil then
|
||||
e.kit, e.kitUntil = nil, nil;
|
||||
e.actor:SetSpellVisualKit();
|
||||
end
|
||||
if e.fearUntil and t >= e.fearUntil then
|
||||
e.fearUntil, e.fearTick = nil, nil;
|
||||
end
|
||||
if e.weakUntil and t >= e.weakUntil then
|
||||
e.weakUntil, e.weakShare = nil, nil;
|
||||
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.
|
||||
local roll = math.random() * (open * (open + 1) / 2);
|
||||
for i = 1, open do
|
||||
local w = open - i + 1;
|
||||
if roll < w then
|
||||
return KINDS[i];
|
||||
end
|
||||
roll = roll - w;
|
||||
end
|
||||
return KINDS[1];
|
||||
end
|
||||
|
||||
-- For the files that load after this one.
|
||||
BW.Blast = Blast
|
||||
BW.Hold = Hold
|
||||
BW.Fear = Fear
|
||||
BW.Weaken = Weaken
|
||||
BW.UpdateStates = UpdateStates
|
||||
BW.BiteShare = BiteShare
|
||||
BW.Spawn = Spawn
|
||||
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,614 @@
|
||||
-- Broodwatch: hold Onyxia's Lair against an endless brood.
|
||||
--
|
||||
-- You run with W A S D (in screen directions). Your spells cast themselves.
|
||||
-- Whelps pour into the lair and chase you; each kill drops a gem, and gems
|
||||
-- level you up. At each level you pick one of three upgrades (click, or
|
||||
-- press 1 2 3). An elite dragonspawn joins every 90 seconds. Survive ten
|
||||
-- minutes to win.
|
||||
--
|
||||
-- /broodwatch starts a run. P pauses. Escape leaves.
|
||||
--
|
||||
-- Prototype: one ClassicAPI ModelScene, the lair as its WMO, every unit an
|
||||
-- actor. The frame time and the Lua time per frame show in the corner.
|
||||
|
||||
local _, BW = ...
|
||||
local Blast = BW.Blast
|
||||
local TUNING = BW.TUNING
|
||||
local LAIR_MAP = BW.LAIR_MAP
|
||||
local CENTER = BW.CENTER
|
||||
local FAR_CLIP = BW.FAR_CLIP
|
||||
local CAMERA_YAW = BW.CAMERA_YAW
|
||||
local CAMERA_PITCH = BW.CAMERA_PITCH
|
||||
local CAMERA_ZOOM = BW.CAMERA_ZOOM
|
||||
local RUN_SECONDS = BW.RUN_SECONDS
|
||||
local ELITE_EVERY = BW.ELITE_EVERY
|
||||
local RETARGET_SECONDS = BW.RETARGET_SECONDS
|
||||
local SURROUND_RADIUS = BW.SURROUND_RADIUS
|
||||
local HERO_HP = BW.HERO_HP
|
||||
local MAGNET = BW.MAGNET
|
||||
local GEM_SPEED = BW.GEM_SPEED
|
||||
local GEM_TAKE = BW.GEM_TAKE
|
||||
local ELITE = BW.ELITE
|
||||
local SPELLS = BW.SPELLS
|
||||
local 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 XpNeeded = BW.XpNeeded
|
||||
local GainXp = BW.GainXp
|
||||
local Taunted = BW.Taunted
|
||||
local UpdateItems = BW.UpdateItems
|
||||
local ClearItems = BW.ClearItems
|
||||
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 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
|
||||
|
||||
local function Steer()
|
||||
if not BW.run or BW.paused or BW.run.over then
|
||||
return;
|
||||
end
|
||||
local forward = Held("W") - Held("S");
|
||||
local left = Held("A") - Held("D");
|
||||
if forward == 0 and left == 0 then
|
||||
if BW.moving then
|
||||
BW.hero:StopMoving();
|
||||
BW.moving = false;
|
||||
end
|
||||
return;
|
||||
end
|
||||
local yaw = BW.camera:GetYaw() + math.atan2(left, forward);
|
||||
if not BW.moving or math.abs(yaw - BW.run.heroYaw) > 0.001 then
|
||||
BW.run.heroYaw = yaw;
|
||||
BW.hero:SetYaw(yaw);
|
||||
end
|
||||
if not BW.moving then
|
||||
BW.hero:StartMovingForward();
|
||||
BW.moving = true;
|
||||
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("The lair holds!");
|
||||
else
|
||||
BW.bannerText:SetText("You fell at " .. Clock(BW.run.time));
|
||||
end
|
||||
BW.hintText:SetText(string.format("Level %d, %d kills. R: try again. Escape: leave.", BW.run.level, BW.run.kills));
|
||||
end
|
||||
|
||||
BW.heroNumber = {} -- ShowNumber's stand-in record for the hero
|
||||
local HEAL_COLOR = { 0.3, 1, 0.3 }
|
||||
|
||||
local function StartRun()
|
||||
if BW.run then
|
||||
ClearEnemies();
|
||||
ClearGems();
|
||||
ClearNumbers();
|
||||
ClearTotems();
|
||||
ClearAreas();
|
||||
ClearLandings();
|
||||
ClearItems();
|
||||
ClearWards();
|
||||
ClearPet();
|
||||
end
|
||||
BW.hero:SetSpellVisualKit();
|
||||
BW.hurtTexture:SetAlpha(0);
|
||||
BW.run = {
|
||||
time = 0, kills = 0, alive = 0, level = 1, xp = 0, pendingLevels = 0,
|
||||
hp = HERO_HP, damageScale = 1, heroYaw = 0,
|
||||
spells = { frostbolt = { level = 1, cooldown = 0, pending = 0 } },
|
||||
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, leech = 0, landings = {},
|
||||
};
|
||||
SetPaused(false);
|
||||
BW.held = {};
|
||||
BW.moving = false;
|
||||
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:SetMovementEnabled(true);
|
||||
BW.run.heroRun = BW.hero:GetMovementSpeed("run");
|
||||
BW.cards:Hide();
|
||||
BW.bannerText:SetText("");
|
||||
BW.hintText:SetText("");
|
||||
BW.xpBar:SetMinMaxValues(0, XpNeeded(1), AT_ONCE);
|
||||
BW.xpBar:SetValue(0, AT_ONCE);
|
||||
UpdateHealth(AT_ONCE);
|
||||
UpdateBuild();
|
||||
BW.camera:SetZoomDistance(CAMERA_ZOOM);
|
||||
BW.camera:SnapAllInterpolatedValues();
|
||||
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.
|
||||
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 not (e.stunUntil and BW.run.time < e.stunUntil) and not e.actor:IsFalling() then
|
||||
if 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);
|
||||
else
|
||||
e.actor:MoveTo(hx + SURROUND_RADIUS * math.cos(e.angle), hy + SURROUND_RADIUS * math.sin(e.angle), hz);
|
||||
end
|
||||
end
|
||||
BW.run.retargetCursor = BW.run.retargetCursor + 1;
|
||||
end
|
||||
end
|
||||
|
||||
local function Step(dt)
|
||||
BW.run.time = BW.run.time + dt;
|
||||
local hx, hy, hz = BW.hero:GetPosition();
|
||||
|
||||
-- Spawns.
|
||||
BW.run.spawnGap = BW.run.spawnGap - dt;
|
||||
if BW.run.spawnGap <= 0 then
|
||||
BW.run.spawnGap = 0.1;
|
||||
local want = TargetAlive(BW.run.time) - BW.run.alive;
|
||||
for _ = 1, math.min(want, 5) do
|
||||
Spawn(PickKind());
|
||||
end
|
||||
end
|
||||
if BW.run.time >= BW.run.nextElite then
|
||||
BW.run.nextElite = BW.run.nextElite + ELITE_EVERY;
|
||||
for _ = 1, 1 + math.floor(BW.run.time / 180) do
|
||||
Spawn(ELITE);
|
||||
end
|
||||
end
|
||||
|
||||
Chase(dt, hx, hy, hz);
|
||||
|
||||
-- Contact damage, and the corpses. Thorns burns the biters once a second.
|
||||
local hurt = 0;
|
||||
local thorns = false;
|
||||
if BW.run.thornsUntil then
|
||||
if BW.run.time >= BW.run.thornsUntil then
|
||||
BW.run.thornsUntil = nil;
|
||||
else
|
||||
BW.run.thornsTick = BW.run.thornsTick - dt;
|
||||
if BW.run.thornsTick <= 0 then
|
||||
BW.run.thornsTick = BW.run.thornsTick + 1;
|
||||
thorns = true;
|
||||
end
|
||||
end
|
||||
end
|
||||
for i = table.getn(enemies), 1, -1 do
|
||||
local e = enemies[i];
|
||||
if e.lit then
|
||||
e.flash = e.flash - dt;
|
||||
if e.flash <= 0 then
|
||||
Unflash(e);
|
||||
end
|
||||
end
|
||||
if e.alive and e.dotLeft then
|
||||
e.dotTick = e.dotTick - dt;
|
||||
if e.dotTick <= 0 then
|
||||
e.dotTick = e.dotTick + 1;
|
||||
e.dotLeft = e.dotLeft - 1;
|
||||
-- The last tick can kill too, and then it still spreads (Kill
|
||||
-- reads dotLeft), so it ends after the damage.
|
||||
Damage(e, e.dotDamage, SPELLS.corruption.color);
|
||||
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 not Taunted(e) and not e.fearUntil 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);
|
||||
if thorns then
|
||||
Damage(e, BW.run.thornsDamage, SPELLS.thorns.color);
|
||||
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.
|
||||
e.dying = e.dying - dt;
|
||||
if e.dying <= 0 then
|
||||
if not e.fading then
|
||||
e.fading = true;
|
||||
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.corruption.hop, IsCorrupted);
|
||||
if not e then
|
||||
break;
|
||||
end
|
||||
Corrupt(e, from.dotDamage, from.dotSeconds, from.dotSpread);
|
||||
end
|
||||
end
|
||||
UpdateTotems(dt);
|
||||
UpdateAreas(dt);
|
||||
UpdateLandings(dt);
|
||||
UpdateItems(dt, hx, hy, hz);
|
||||
UpdateWards(dt);
|
||||
UpdatePet(dt, hx, hy, hz);
|
||||
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.
|
||||
local taken = hurt * dt;
|
||||
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 > 0 then
|
||||
local heal = math.floor(BW.run.pendingHeal + 0.5);
|
||||
BW.run.pendingHeal = 0;
|
||||
if heal > 0 and BW.run.hp < MaxHp() then
|
||||
BW.run.hp = math.min(MaxHp(), BW.run.hp + heal);
|
||||
UpdateHealth();
|
||||
BW.heroNumber.actor = BW.hero;
|
||||
ShowNumber(BW.heroNumber, heal, HEAL_COLOR, "+" .. heal);
|
||||
end
|
||||
end
|
||||
|
||||
local regen = PASSIVES.renewal.per * (BW.run.passives.renewal or 0);
|
||||
if taken > 0 or (regen > 0 and BW.run.hp < MaxHp()) then
|
||||
BW.run.hp = math.min(MaxHp(), BW.run.hp + regen * dt - taken);
|
||||
UpdateHealth();
|
||||
if BW.run.hp <= 0 and BW.run.passives.soulstone and not BW.run.soulstoneUsed then
|
||||
-- Rise again at half health, and throw back the brood close by.
|
||||
BW.run.soulstoneUsed = true;
|
||||
BW.run.hp = MaxHp() * 0.5;
|
||||
UpdateHealth();
|
||||
BW.hero:PlaySpellVisual(SOULSTONE_SPELL, BW.hero, 0);
|
||||
local r2 = SOULSTONE_BLAST * SOULSTONE_BLAST;
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
if Dist2(x, y, hx, hy) <= r2 then
|
||||
if e.kind ~= ELITE then
|
||||
Kill(e);
|
||||
end
|
||||
Blast(e, hx, hy, TUNING.SOULSTONE_KNOCK);
|
||||
end
|
||||
end
|
||||
end
|
||||
UpdateBuild();
|
||||
end
|
||||
if BW.run.hp <= 0 then
|
||||
EndRun(false);
|
||||
return;
|
||||
end
|
||||
end
|
||||
|
||||
-- Gems.
|
||||
local magnet = MAGNET * (1 + 0.4 * (BW.run.passives.reach or 0));
|
||||
local m2 = magnet * magnet;
|
||||
local take2 = GEM_TAKE * GEM_TAKE;
|
||||
local gained = 0;
|
||||
for i = table.getn(gems), 1, -1 do
|
||||
local gem = gems[i];
|
||||
local d2 = Dist2(gem.x, gem.y, hx, hy);
|
||||
if not gem.pulled and d2 <= m2 then
|
||||
gem.pulled = true;
|
||||
end
|
||||
if gem.pulled then
|
||||
if d2 <= take2 then
|
||||
gained = gained + gem.xp;
|
||||
gem.actor:Hide();
|
||||
tinsert(gemPool, gem);
|
||||
table.remove(gems, i);
|
||||
else
|
||||
-- It flies at you, faster than you run, and rises to your chest.
|
||||
local d = math.sqrt(d2);
|
||||
local step = math.min(1, GEM_SPEED * dt / d);
|
||||
gem.x = gem.x + (hx - gem.x) * step;
|
||||
gem.y = gem.y + (hy - gem.y) * step;
|
||||
gem.z = gem.z + (hz + 1 - gem.z) * step;
|
||||
gem.actor:SetPosition(gem.x, gem.y, gem.z);
|
||||
end
|
||||
end
|
||||
end
|
||||
if gained > 0 then
|
||||
GainXp(gained * (1 + PASSIVES.wisdom.per * (BW.run.passives.wisdom or 0) / 100));
|
||||
end
|
||||
|
||||
-- Spells.
|
||||
local haste = 1 - 0.08 * (BW.run.passives.haste or 0);
|
||||
for key, state in pairs(BW.run.spells) do
|
||||
state.cooldown = state.cooldown - dt;
|
||||
if state.cooldown <= 0 and state.pending == 0 and Fire(key, state) then
|
||||
state.cooldown = Stat(SPELLS[key], "cooldown", state.level) * haste;
|
||||
state.cdFull = state.cooldown;
|
||||
if state.slot then
|
||||
state.slot.cd:SetCooldown(GetTime(), state.cooldown);
|
||||
end
|
||||
end
|
||||
end
|
||||
BW.run.castGap = BW.run.castGap - dt;
|
||||
if BW.run.castGap <= 0 then
|
||||
CastNext();
|
||||
end
|
||||
|
||||
if BW.run.time >= RUN_SECONDS then
|
||||
EndRun(true);
|
||||
end
|
||||
end
|
||||
|
||||
local function UpdateHud(dt, luaMs)
|
||||
local second = math.floor(BW.run.time);
|
||||
if second ~= BW.run.shownSecond then
|
||||
BW.run.shownSecond = second;
|
||||
BW.infoText:SetText(string.format("Level %d %s Kills %d", BW.run.level, 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.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.healthBar = getglobal(hud .. "Health");
|
||||
BW.healthText = getglobal(hud .. "HealthText");
|
||||
BW.cards = getglobal(hud .. "Cards");
|
||||
BW.build = getglobal(hud .. "Build");
|
||||
self:SetScript("OnUpdate", OnUpdate);
|
||||
self:SetScript("OnKeyDown", OnKeyDown);
|
||||
self:SetScript("OnKeyUp", OnKeyUp);
|
||||
self:SetScript("OnShow", function()
|
||||
BW.frame:EnableKeyboard(true);
|
||||
end);
|
||||
self:SetScript("OnHide", function()
|
||||
BW.frame:EnableKeyboard(false);
|
||||
if BW.run and not BW.run.over then
|
||||
EndRun(false);
|
||||
end
|
||||
end);
|
||||
end
|
||||
|
||||
SLASH_BROODWATCH1 = "/broodwatch";
|
||||
SlashCmdList["BROODWATCH"] = function()
|
||||
BW.frame:Show();
|
||||
if not BW.hero then
|
||||
Build();
|
||||
end
|
||||
StartRun();
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
## 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
|
||||
Spells.lua
|
||||
Hud.lua
|
||||
Items.lua
|
||||
Broodwatch.lua
|
||||
Broodwatch.xml
|
||||
@@ -0,0 +1,227 @@
|
||||
<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">
|
||||
<FontString name="$parentName" inherits="GameFontNormalLarge">
|
||||
<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="-30"/>
|
||||
</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>
|
||||
<StatusBar name="$parentXP">
|
||||
<Size x="10" y="12"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" x="8" y="-8"/>
|
||||
<Anchor point="TOPRIGHT" x="-8" y="-8"/>
|
||||
</Anchors>
|
||||
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
|
||||
<BarColor r="0.58" g="0.0" b="0.55"/>
|
||||
</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,105 @@
|
||||
-- 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
|
||||
|
||||
-- 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
|
||||
|
||||
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
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- 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.DropGem = DropGem
|
||||
BW.ClearGems = ClearGems
|
||||
@@ -0,0 +1,346 @@
|
||||
-- Broodwatch tuning and data: the game's constants, spells, passives and items.
|
||||
|
||||
local _, BW = ...
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Tuning
|
||||
|
||||
local LAIR_MAP = 249
|
||||
local CENTER = { 0.7, -211.97, -86.08 } -- Onyxia's spawn point, the room's floor
|
||||
local ROOM_RADIUS = 22 -- yards around CENTER where the brood spawns
|
||||
local SPAWN_MIN_DISTANCE = 13 -- yards from the hero
|
||||
local FAR_CLIP = 500
|
||||
|
||||
local CAMERA_YAW = 3.09
|
||||
local CAMERA_PITCH = 1.0
|
||||
local CAMERA_ZOOM = 50 -- each run starts zoomed out; the wheel zooms in
|
||||
|
||||
local RUN_SECONDS = 600
|
||||
local ELITE_EVERY = 90
|
||||
local MAX_ALIVE = 200
|
||||
local RETARGET_SECONDS = 0.3 -- each chaser gets a new MoveTo this often
|
||||
local SURROUND_RADIUS = 1.0 -- chasers aim at a ring around the hero, not its centre
|
||||
local CAST_SPACING = 0.3 -- seconds between two of the hero's casts
|
||||
local DEATH_SECONDS = 4.5 -- the death animation (1.5 s), then the corpse lies; then it fades out
|
||||
local ANIM_STAND = 0
|
||||
local ANIM_DEATH = 1
|
||||
|
||||
local HERO_HP = 100
|
||||
local MAGNET = 5 -- gems this close (yards) fly to you
|
||||
local GEM_SPEED = 20 -- yards a second, once pulled
|
||||
local GEM_TAKE = 0.8 -- a pulled gem this close is taken
|
||||
local GEM_PATH = "World\\Generic\\ActiveDoodads\\Rocks\\GemRock01.m2"
|
||||
local MAX_GEMS = 150
|
||||
|
||||
-- The brood. `from` is the second of the run when the kind starts to spawn.
|
||||
local KINDS = {
|
||||
{ key = "black", display = 497, hp = 20, dps = 5, xp = 1, reach = 1.4, speed = 3.0, from = 0 },
|
||||
{ key = "red", display = 956, hp = 30, dps = 6, xp = 2, reach = 1.4, speed = 4.4, from = 120 },
|
||||
{ key = "green", display = 694, hp = 70, dps = 9, xp = 3, reach = 1.4, speed = 2.4, from = 240 },
|
||||
}
|
||||
-- `speed` is the run speed in yards a second (the hero runs at 7): black
|
||||
-- whelps plod, red ones dart, green ones are slow and tough, and the elite
|
||||
-- is faster than any whelp but still slower than you.
|
||||
local ELITE = { key = "elite", display = 2534, hp = 220, dps = 9, xp = 20, reach = 2.2, speed = 4.8, gemScale = 0.6 }
|
||||
|
||||
-- Spells, by level 1 to 5. A missile spell hits `count` enemies in `range`;
|
||||
-- a nova spell hits every enemy within `radius` of the hero. At rank V a
|
||||
-- spell masters: `mastery` is what it does then, and its numbers.
|
||||
local SPELLS = {
|
||||
frostbolt = {
|
||||
spell = 116, blurb = "Bolts the nearest enemy.", color = { 0.55, 0.8, 1 }, kind = "missile", pick = "nearest", range = 24,
|
||||
damage = { 14, 18, 18, 24, 26 }, count = { 1, 1, 2, 2, 3 }, cooldown = { 1.2, 1.05, 1.05, 0.9, 0.8 },
|
||||
mastery = { text = "Frostbite: each bolt freezes its target for 1 second.", freeze = 1 },
|
||||
},
|
||||
shadowbolt = {
|
||||
spell = 686, blurb = "A heavy bolt at the toughest enemy in range.", color = { 0.8, 0.45, 1 }, kind = "missile", pick = "strongest", range = 26,
|
||||
damage = { 45, 60, 75, 75, 100 }, count = { 1, 1, 1, 2, 2 }, cooldown = { 2.6, 2.4, 2.2, 2.2, 2.0 },
|
||||
mastery = { text = "Shadowburn: a bolt that kills bursts, hitting enemies within 4 yards for half its damage.", radius = 4, share = 0.5 },
|
||||
},
|
||||
lightning = {
|
||||
spell = 403, blurb = "Strikes enemies at random.", color = { 1, 0.95, 0.4 }, kind = "missile", pick = "random", range = 22,
|
||||
damage = { 22, 28, 34, 40, 48 }, count = { 1, 2, 2, 3, 4 }, cooldown = { 1.8, 1.7, 1.6, 1.5, 1.3 },
|
||||
mastery = { text = "Overload: each bolt has a 1 in 3 chance to strike another enemy.", chance = 1 / 3 },
|
||||
},
|
||||
nova = {
|
||||
spell = 1449, blurb = "Bursts arcane power at every enemy around you.", color = { 0.85, 0.55, 1 }, kind = "nova",
|
||||
damage = { 10, 14, 18, 22, 28 }, radius = { 6, 6.5, 7, 7.5, 8 }, cooldown = { 5, 4.6, 4.2, 3.8, 3.4 },
|
||||
mastery = { text = "Echo: each explosion bursts a second time." },
|
||||
},
|
||||
chain = {
|
||||
spell = 421, blurb = "Leaps from enemy to enemy.",
|
||||
color = { 0.6, 0.75, 1 }, kind = "chain", pick = "nearest", range = 22, hop = 8,
|
||||
damage = { 20, 24, 30, 36, 44 }, jumps = { 3, 4, 4, 5, 6 }, cooldown = { 2.4, 2.2, 2.0, 1.8, 1.6 },
|
||||
mastery = { text = "Each leap strikes 20% harder than the one before.", grow = 1.2 },
|
||||
},
|
||||
corruption = {
|
||||
spell = 172, blurb = "Rots enemies every second, and spreads when they die.",
|
||||
color = { 0.55, 0.85, 0.3 }, kind = "dot", pick = "nearest", range = 22, hop = 6,
|
||||
damage = { 6, 8, 10, 12, 15 }, count = { 2, 3, 3, 4, 5 }, spread = { 1, 1, 2, 2, 3 },
|
||||
duration = { 6, 6, 7, 7, 8 }, cooldown = { 3, 3, 2.8, 2.6, 2.4 },
|
||||
mastery = { text = "Corrupted enemies take 25% more damage from everything.", amplify = 1.25 },
|
||||
},
|
||||
holynova = {
|
||||
spell = 15237, blurb = "Burns enemies around you, and heals you for each one.",
|
||||
color = { 1, 0.95, 0.6 }, kind = "nova",
|
||||
damage = { 12, 16, 20, 26, 32 }, radius = { 6, 6.5, 7, 7.5, 8 }, heal = { 1, 1, 1.5, 1.5, 2 },
|
||||
cooldown = { 4, 3.7, 3.4, 3.1, 2.8 },
|
||||
mastery = { text = "Enemies it burns are dazed: half speed for 2 seconds.", slow = 0.5, seconds = 2 },
|
||||
},
|
||||
-- A fire totem model (display 4589, FireElementalTotem.mdx) that pulses
|
||||
-- as the real Magma Totem does (its pulse spell 8187, whose visual puts an
|
||||
-- Immolate impact on each enemy it burns).
|
||||
totem = {
|
||||
spell = 8190, pulse = 8187, display = 4589,
|
||||
blurb = "Plants a totem that burns every enemy near it every 2 seconds.",
|
||||
color = { 1, 0.5, 0.15 }, kind = "totem", pulseEvery = 2,
|
||||
damage = { 14, 18, 23, 28, 35 }, radius = { 6, 6.5, 7, 7.5, 8 }, totems = { 1, 1, 2, 2, 3 },
|
||||
duration = { 12, 12, 14, 14, 16 }, cooldown = { 14, 13, 12, 11, 10 },
|
||||
mastery = { text = "When a totem's time runs out, it erupts for triple damage.", burst = 3 },
|
||||
},
|
||||
-- Ground areas: the spell's own area model (SetModelBySpellArea) stands
|
||||
-- where it lands and burns every enemy inside it each second. An area
|
||||
-- with `slow` also slows them to that share of their speed.
|
||||
blizzard = {
|
||||
spell = 10, blurb = "Ice storms down on the thickest pack and chills it.",
|
||||
color = { 0.55, 0.8, 1 }, kind = "area", place = "pack", range = 22,
|
||||
damage = { 8, 10, 13, 16, 20 }, radius = { 5, 5.5, 6, 6.5, 7 }, slow = { 0.7, 0.65, 0.6, 0.55, 0.5 },
|
||||
duration = { 4, 4, 5, 5, 6 },
|
||||
cooldown = { 6, 5.5, 5, 4.5, 4 },
|
||||
mastery = { text = "Deep Freeze: when the storm ends, it freezes everything inside for 2 seconds.", freeze = 2 },
|
||||
},
|
||||
rainoffire = {
|
||||
spell = 5740, blurb = "Fire rains on the thickest pack.",
|
||||
color = { 1, 0.45, 0.15 }, kind = "area", place = "pack", range = 22,
|
||||
damage = { 14, 18, 23, 28, 35 }, radius = { 4, 4.4, 4.8, 5.2, 6 }, duration = { 3, 3, 3, 4, 4 },
|
||||
cooldown = { 7, 6.5, 6, 5.5, 5 },
|
||||
mastery = { text = "A second storm falls on another enemy." },
|
||||
},
|
||||
consecration = {
|
||||
spell = 26573, blurb = "Hallows the ground at your feet.",
|
||||
color = { 1, 0.85, 0.4 }, kind = "area", place = "self",
|
||||
damage = { 6, 8, 10, 13, 16 }, radius = { 5, 5.5, 6, 6.5, 7 }, duration = { 6, 6, 7, 7, 8 },
|
||||
cooldown = { 7, 6.5, 6, 5.5, 5 },
|
||||
mastery = { text = "The hallowed ground moves with you." },
|
||||
},
|
||||
thunder = {
|
||||
spell = 6343, blurb = "Shakes the ground close around you.", color = { 1, 0.6, 0.2 }, kind = "nova",
|
||||
damage = { 16, 20, 26, 32, 40 }, radius = { 4, 4.4, 4.8, 5.2, 6 }, cooldown = { 3, 2.8, 2.6, 2.4, 2.2 },
|
||||
mastery = { text = "Aftershock: the clap throws the brood back.", knock = { 8, 5 } },
|
||||
},
|
||||
-- Tanky spells keep the brood off you. `kit` is a SpellVisualKit shown
|
||||
-- while the effect lasts: the shield's bubble, Frost Nova's ice on each
|
||||
-- frozen enemy, Psychic Scream's terror over each fleeing one.
|
||||
shield = {
|
||||
spell = 17, kit = 847, blurb = "A shield soaks up the brood's bites until it breaks.",
|
||||
color = { 1, 1, 0.75 }, kind = "shield",
|
||||
absorb = { 30, 40, 50, 60, 75 }, duration = { 10, 10, 12, 12, 15 }, cooldown = { 15, 14, 13, 12, 11 },
|
||||
mastery = { text = "When the shield breaks, it bursts and hurls the brood around you away.", radius = 6, damage = 30, knock = { 12, 8 } },
|
||||
},
|
||||
thorns = {
|
||||
spell = 467, blurb = "Every enemy biting you takes damage each second.",
|
||||
color = { 0.6, 0.9, 0.3 }, kind = "thorns",
|
||||
damage = { 6, 8, 10, 13, 16 }, duration = { 8, 9, 10, 11, 12 }, cooldown = { 14, 13, 12, 11, 10 },
|
||||
mastery = { text = "You heal 1 health for every biter it burns.", heal = 1 },
|
||||
},
|
||||
frostnova = {
|
||||
spell = 122, kit = 285, blurb = "Freezes every enemy around you in place.",
|
||||
color = { 0.6, 0.85, 1 }, kind = "nova",
|
||||
damage = { 6, 8, 10, 12, 15 }, radius = { 6, 6.5, 7, 7.5, 8 }, root = { 2, 2.5, 3, 3.5, 4 },
|
||||
cooldown = { 10, 9.5, 9, 8.5, 8 },
|
||||
mastery = { text = "Shatter: frozen enemies take double damage.", amplify = 2 },
|
||||
},
|
||||
scream = {
|
||||
spell = 8122, kit = 4031, blurb = "Every enemy around you flees in terror.",
|
||||
color = { 0.75, 0.5, 1 }, kind = "nova",
|
||||
radius = { 6, 6.5, 7, 7.5, 8 }, fear = { 2, 2.5, 3, 3.5, 4 }, cooldown = { 14, 13, 12, 11, 10 },
|
||||
mastery = { text = "Terror: fleeing enemies take 10 damage a second.", damage = 10 },
|
||||
},
|
||||
demoshout = {
|
||||
spell = 1160, blurb = "Enemies around you bite softer for a while.",
|
||||
color = { 1, 0.55, 0.35 }, kind = "nova",
|
||||
radius = { 8, 8.5, 9, 9.5, 10 }, weaken = { 0.3, 0.35, 0.4, 0.45, 0.5 }, duration = { 6, 7, 8, 9, 10 },
|
||||
cooldown = { 10, 9.5, 9, 8.5, 8 },
|
||||
mastery = { text = "Battle cry: you run 20% faster while it lasts.", speed = 1.2 },
|
||||
},
|
||||
stoneclaw = {
|
||||
spell = 5730, display = 4588, blurb = "Plants a totem that the brood around it must attack.",
|
||||
color = { 0.8, 0.65, 0.4 }, kind = "ward",
|
||||
radius = { 6, 7, 8, 9, 10 }, duration = { 5, 6, 7, 8, 9 }, cooldown = { 16, 15, 14, 13, 12 },
|
||||
mastery = { text = "When the totem crumbles, it stuns the brood around it for 2 seconds.", stun = 2 },
|
||||
},
|
||||
-- Fun ones. `knock` is the knockback (out, up), as the items' are.
|
||||
blastwave = {
|
||||
spell = 11113, blurb = "A ring of fire hurls the brood away from you.",
|
||||
color = { 1, 0.5, 0.2 }, kind = "nova", knock = { 10, 7 },
|
||||
damage = { 18, 24, 30, 38, 48 }, radius = { 6, 6.5, 7, 7.5, 8 }, cooldown = { 7, 6.6, 6.2, 5.8, 5.4 },
|
||||
mastery = { text = "The brood it hits is dazed: half speed for 3 seconds.", slow = 0.5, seconds = 3 },
|
||||
},
|
||||
-- The imp (display 4449) stays for the run and casts the real Firebolt.
|
||||
imp = {
|
||||
spell = 688, missile = 3110, display = 4449, blurb = "An imp follows you and hurls Firebolts.",
|
||||
color = { 1, 0.55, 0.2 }, kind = "pet", range = 22,
|
||||
damage = { 12, 15, 19, 24, 30 }, cooldown = { 1.6, 1.5, 1.4, 1.3, 1.2 },
|
||||
mastery = { text = "The imp hurls two Firebolts at once.", bolts = 2 },
|
||||
},
|
||||
}
|
||||
local SPELL_ORDER = {
|
||||
"frostbolt", "shadowbolt", "lightning", "chain", "corruption", "nova", "holynova", "thunder", "totem", "blizzard",
|
||||
"rainoffire", "consecration", "shield", "thorns", "frostnova", "scream", "demoshout", "stoneclaw", "blastwave", "imp",
|
||||
}
|
||||
local MAX_SPELLS = 6
|
||||
local EARLY_SPELLS = 3 -- until you have this many spells, each level offers a new one
|
||||
|
||||
-- `icon` is the spell whose icon the passive shows; `total` is what all its
|
||||
-- ranks give together.
|
||||
local PASSIVES = {
|
||||
might = { name = "Might", icon = 19740, text = "+15% damage", total = "+%d%% damage", per = 15 },
|
||||
haste = { name = "Haste", icon = 12043, text = "-8% spell cooldowns", total = "-%d%% cooldowns", per = 8 },
|
||||
reach = { name = "Reach", icon = 2481, text = "+40% gem pickup radius", total = "+%d%% pickup radius", per = 40 },
|
||||
vigor = { name = "Vigor", icon = 1243, text = "+25 maximum health, and heal 25", total = "+%d maximum health", per = 25 },
|
||||
renewal = { name = "Renewal", icon = 139, text = "+1.2 health per second", total = "%.1f health per second", per = 1.2 },
|
||||
sprint = { icon = 2983, text = "+8% run speed", total = "+%d%% run speed", per = 8 },
|
||||
devotion = { icon = 465, text = "-5% damage taken", total = "-%d%% damage taken", per = 5 },
|
||||
amplify = { icon = 1008, text = "+10% spell radius", total = "+%d%% spell radius", per = 10 },
|
||||
multishot = {
|
||||
icon = 2643, text = "+1 target for bolts and Corruption", total = "+%d targets", per = 1,
|
||||
max = 3,
|
||||
},
|
||||
wisdom = { icon = 19742, text = "+15% experience", total = "+%d%% experience", per = 15 },
|
||||
soulstone = {
|
||||
name = "Soulstone", icon = 20707, text = "Once per run, rise again with half your health",
|
||||
total = "%d revive", per = 1, max = 1,
|
||||
},
|
||||
vampiric = {
|
||||
icon = 15286,
|
||||
text = "Heals you for 0.2% of the damage you deal, at most 2 health a second",
|
||||
total = "up to %d health a second", per = 2,
|
||||
},
|
||||
}
|
||||
-- The names are the client's own, in its language: each spell's from its
|
||||
-- spell, and each passive without a name of its own from its icon's spell.
|
||||
for _, def in pairs(SPELLS) do
|
||||
def.name = C_Spell.GetSpellName(def.spell)
|
||||
end
|
||||
for _, p in pairs(PASSIVES) do
|
||||
p.name = p.name or C_Spell.GetSpellName(p.icon)
|
||||
end
|
||||
|
||||
local HEAL_ICON = 5185 -- Healing Touch
|
||||
local UNKNOWN_ICON = "Interface\\Icons\\INV_Misc_QuestionMark"
|
||||
local PASSIVE_ORDER = {
|
||||
"might", "haste", "reach", "vigor", "renewal", "sprint", "vampiric", "devotion", "amplify", "multishot",
|
||||
"wisdom", "soulstone",
|
||||
}
|
||||
local VAMPIRIC_SHARE = 0.002 -- of the damage dealt, per rank
|
||||
local HOLY_NOVA_HEAL_TARGETS = 8 -- Holy Nova heals for at most this many enemies
|
||||
local SOULSTONE_SPELL = 20707 -- Soulstone Resurrection, the revive's visual
|
||||
local SOULSTONE_BLAST = 10 -- yards: rising again throws back the enemies this close
|
||||
local MAX_LEVEL = 5
|
||||
|
||||
local ROMAN = { "I", "II", "III", "IV", "V" }
|
||||
|
||||
-- The bars ease to a new value in the engine (StatusBar interpolation); a
|
||||
-- reset (a new run, a level's wrap) shows at once.
|
||||
local EASE = Enum.StatusBarInterpolation.ExponentialEaseOut
|
||||
local AT_ONCE = Enum.StatusBarInterpolation.Immediate
|
||||
|
||||
-- The items' tuning.
|
||||
local TUNING = {
|
||||
SWIFTNESS_FACTOR = 1.5,
|
||||
SWIFTNESS_SECONDS = 10,
|
||||
ITEM_CHANCE = 1 / 150, -- a whelp's chance to drop an item
|
||||
ITEM_REACH = 1.5, -- yards: walking this close uses a drop
|
||||
ITEM_SECONDS = 30, -- a drop lies this long
|
||||
ITEM_ICON = 28, -- pixels
|
||||
ITEM_COLOR = { 1, 0.82, 0 },
|
||||
POTION_HEAL = 0.3, -- of the maximum health
|
||||
GRENADE_RADIUS = 5,
|
||||
GRENADE_DAMAGE = 45, -- grows with the run as the brood's health does
|
||||
GRENADE_STUN = 2, -- seconds
|
||||
SAPPER_RADIUS = 10,
|
||||
SAPPER_SELF = 0.15, -- of the maximum health; it never kills you
|
||||
SAPPER_ELITE = 0.25, -- of an elite's maximum health
|
||||
CHICKEN_DISPLAY = 6909, -- the robot chicken (ChickenSkinRobot)
|
||||
CHICKEN_SECONDS = 20,
|
||||
CHICKEN_SPEED = 9,
|
||||
CHICKEN_DAMAGE = 12, -- a peck, grows with the run
|
||||
CHICKEN_PECK = 0.8, -- seconds between pecks
|
||||
CHICKEN_REACH = 1.8,
|
||||
CHICKEN_LEASH = 20, -- yards from the hero: it hunts no farther
|
||||
CHICKEN_HUNT_MIN = 6, -- yards from the hero: it prefers prey past this
|
||||
CHICKEN_TAUNT = 3, -- seconds a peck holds the whelps around it
|
||||
CHICKEN_TAUNT_RADIUS = 3, -- yards around the chicken that a peck taunts
|
||||
CHICKEN_ROAM = 6, -- yards around the hero that it roams with no prey
|
||||
-- Knockbacks (actor:KnockBack): yards a second out and up.
|
||||
GRENADE_KNOCK = { 10, 7 },
|
||||
SOULSTONE_KNOCK = { 16, 10 },
|
||||
ELITE_KNOCK_SHARE = 0.4, -- an elite takes this share of a knockback
|
||||
ELITE_HOLD_SHARE = 0.5, -- and this share of a root's or a fear's time
|
||||
FEAR_STEP = 6, -- yards a fleeing enemy runs before its next MoveTo
|
||||
IMP_SPEED = 8.5,
|
||||
IMP_FOLLOW = 3, -- yards: farther from you than this, the imp follows
|
||||
}
|
||||
-- `itemID` is the real item (its name and icon), `spell` the visual its use plays.
|
||||
local ITEMS = {
|
||||
{ key = "potion", itemID = 929, spell = 17534, weight = 30 },
|
||||
{ key = "grenade", itemID = 15993, spell = 19769, weight = 25 },
|
||||
{ key = "swift", itemID = 2459, spell = 2379, weight = 15 },
|
||||
{ key = "hearth", itemID = 6948, spell = 8690, weight = 10 },
|
||||
{ key = "sapper", itemID = 10646, spell = 13241, weight = 10 },
|
||||
{ key = "chicken", itemID = 10725, spell = 23133, weight = 10 },
|
||||
}
|
||||
-- Each item's name and icon are the client's (the Item mixin), once its data
|
||||
-- has come from the server.
|
||||
for _, item in ipairs(ITEMS) do
|
||||
local data = Item:CreateFromItemID(item.itemID);
|
||||
data:ContinueOnItemLoad(function()
|
||||
item.name = data:GetItemName();
|
||||
item.icon = data:GetItemIcon();
|
||||
end);
|
||||
end
|
||||
|
||||
-- For the files that load after this one.
|
||||
BW.LAIR_MAP = LAIR_MAP
|
||||
BW.CENTER = CENTER
|
||||
BW.ROOM_RADIUS = ROOM_RADIUS
|
||||
BW.SPAWN_MIN_DISTANCE = SPAWN_MIN_DISTANCE
|
||||
BW.FAR_CLIP = FAR_CLIP
|
||||
BW.CAMERA_YAW = CAMERA_YAW
|
||||
BW.CAMERA_PITCH = CAMERA_PITCH
|
||||
BW.CAMERA_ZOOM = CAMERA_ZOOM
|
||||
BW.RUN_SECONDS = RUN_SECONDS
|
||||
BW.ELITE_EVERY = ELITE_EVERY
|
||||
BW.MAX_ALIVE = MAX_ALIVE
|
||||
BW.RETARGET_SECONDS = RETARGET_SECONDS
|
||||
BW.SURROUND_RADIUS = SURROUND_RADIUS
|
||||
BW.CAST_SPACING = CAST_SPACING
|
||||
BW.DEATH_SECONDS = DEATH_SECONDS
|
||||
BW.ANIM_STAND = ANIM_STAND
|
||||
BW.ANIM_DEATH = ANIM_DEATH
|
||||
BW.HERO_HP = HERO_HP
|
||||
BW.MAGNET = MAGNET
|
||||
BW.GEM_SPEED = GEM_SPEED
|
||||
BW.GEM_TAKE = GEM_TAKE
|
||||
BW.GEM_PATH = GEM_PATH
|
||||
BW.MAX_GEMS = MAX_GEMS
|
||||
BW.KINDS = KINDS
|
||||
BW.ELITE = ELITE
|
||||
BW.SPELLS = SPELLS
|
||||
BW.SPELL_ORDER = SPELL_ORDER
|
||||
BW.MAX_SPELLS = MAX_SPELLS
|
||||
BW.EARLY_SPELLS = EARLY_SPELLS
|
||||
BW.PASSIVES = PASSIVES
|
||||
BW.HEAL_ICON = HEAL_ICON
|
||||
BW.UNKNOWN_ICON = UNKNOWN_ICON
|
||||
BW.PASSIVE_ORDER = PASSIVE_ORDER
|
||||
BW.VAMPIRIC_SHARE = VAMPIRIC_SHARE
|
||||
BW.HOLY_NOVA_HEAL_TARGETS = HOLY_NOVA_HEAL_TARGETS
|
||||
BW.SOULSTONE_SPELL = SOULSTONE_SPELL
|
||||
BW.SOULSTONE_BLAST = SOULSTONE_BLAST
|
||||
BW.MAX_LEVEL = MAX_LEVEL
|
||||
BW.ROMAN = ROMAN
|
||||
BW.EASE = EASE
|
||||
BW.AT_ONCE = AT_ONCE
|
||||
BW.TUNING = TUNING
|
||||
BW.ITEMS = ITEMS
|
||||
@@ -0,0 +1,458 @@
|
||||
-- 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 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
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Level-up offers
|
||||
|
||||
local function BuildOffers()
|
||||
local all = {};
|
||||
local owned = 0;
|
||||
for _ in pairs(BW.run.spells) do
|
||||
owned = owned + 1;
|
||||
end
|
||||
for _, key in ipairs(SPELL_ORDER) do
|
||||
local state = BW.run.spells[key];
|
||||
if 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
|
||||
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 },
|
||||
}
|
||||
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",
|
||||
}
|
||||
local STAT_UNITS = { radius = " yd", duration = " s", cooldown = " s", root = " s", fear = " s" }
|
||||
local STAT_ORDER = {
|
||||
"damage", "count", "jumps", "totems", "spread", "heal", "absorb", "radius", "slow", "root", "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
|
||||
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: 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";
|
||||
else
|
||||
icon, title, tag, colorKey = Icon(HEAL_ICON), "Second Wind", "Everything is at rank V", "heal";
|
||||
end
|
||||
local color = 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:SetText(title);
|
||||
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 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
|
||||
|
||||
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);
|
||||
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);
|
||||
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.cd:Clear();
|
||||
slot.icon:SetDesaturated(key == "soulstone" and BW.run.soulstoneUsed and true or false);
|
||||
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 and BW.moving then
|
||||
BW.hero:StopMoving();
|
||||
BW.moving = false;
|
||||
end
|
||||
end
|
||||
|
||||
local function ShowCards()
|
||||
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("Level " .. (BW.run.level - BW.run.pendingLevels + 1));
|
||||
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 == "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 == "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.run.pendingLevels = BW.run.pendingLevels - 1;
|
||||
if BW.run.pendingLevels > 0 then
|
||||
ShowCards();
|
||||
else
|
||||
SetPaused(false);
|
||||
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 then
|
||||
text = text .. string.format(" (+%d)", math.ceil(BW.run.shield));
|
||||
end
|
||||
BW.healthText:SetText(text);
|
||||
end
|
||||
|
||||
local LEVEL_HEAL = 0.12 -- each level heals this share of the maximum health
|
||||
|
||||
local function XpNeeded(level)
|
||||
return 5 + 3 * level + math.floor(level * level / 8);
|
||||
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
|
||||
local levels = BW.run.pendingLevels;
|
||||
BW.run.hp = math.min(MaxHp(), BW.run.hp + MaxHp() * LEVEL_HEAL * levels);
|
||||
UpdateHealth();
|
||||
end
|
||||
local mode = wrapped and AT_ONCE or EASE;
|
||||
BW.xpBar:SetMinMaxValues(0, XpNeeded(BW.run.level), mode);
|
||||
BW.xpBar:SetValue(BW.run.xp, mode);
|
||||
if BW.run.pendingLevels > 0 and not BW.cards:IsShown() then
|
||||
ShowCards();
|
||||
end
|
||||
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
|
||||
@@ -0,0 +1,325 @@
|
||||
-- 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 = 16
|
||||
|
||||
local drops = {}
|
||||
local dropPool = {}
|
||||
local chickens = {}
|
||||
local chickenPool = {}
|
||||
|
||||
local function RunGrowth()
|
||||
local m = BW.run.time / 300;
|
||||
return 1 + 2 * m + m * m;
|
||||
end
|
||||
|
||||
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 grenade lands on the thickest pack: the reticle marks it while it flies.
|
||||
local function ThrowGrenade(item)
|
||||
local x, y, z, target = PackCenter(24, TUNING.GRENADE_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(TUNING.GRENADE_RADIUS);
|
||||
marker.actor:Show();
|
||||
local damage = TUNING.GRENADE_DAMAGE * RunGrowth();
|
||||
local function Explode(entry)
|
||||
ReleaseMarker(marker);
|
||||
local ex, ey = entry.target.actor:GetPosition();
|
||||
local r2 = TUNING.GRENADE_RADIUS * TUNING.GRENADE_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
|
||||
Hold(o, TUNING.GRENADE_STUN);
|
||||
Damage(o, damage, TUNING.ITEM_COLOR);
|
||||
Blast(o, ex, ey, TUNING.GRENADE_KNOCK);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
local castID = BW.hero:PlaySpellVisual(item.spell, target.actor, 0);
|
||||
local entry = { target = target, onImpact = Explode };
|
||||
if castID then
|
||||
BW.run.inFlight[castID] = entry;
|
||||
else
|
||||
Explode(entry);
|
||||
end
|
||||
end
|
||||
|
||||
-- The Sapper Charge goes off on you: it clears the brood close by, and hurts.
|
||||
local function Sapper(item, hx, hy)
|
||||
BW.hero:PlaySpellVisual(item.spell, BW.hero, 0);
|
||||
local r2 = TUNING.SAPPER_RADIUS * TUNING.SAPPER_RADIUS;
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
if Dist2(x, y, hx, hy) <= r2 then
|
||||
if e.kind == ELITE then
|
||||
Damage(e, e.maxHp * TUNING.SAPPER_ELITE / BW.run.damageScale, TUNING.ITEM_COLOR);
|
||||
else
|
||||
Kill(e);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
BW.run.hp = math.max(1, BW.run.hp - MaxHp() * TUNING.SAPPER_SELF);
|
||||
UpdateHealth();
|
||||
end
|
||||
|
||||
local function SummonChicken(item, hx, hy, hz)
|
||||
BW.hero:PlaySpellVisual(item.spell, BW.hero, 0);
|
||||
local chicken = table.remove(chickenPool);
|
||||
if not chicken then
|
||||
chicken = { actor = BW.scene:AcquireActor() };
|
||||
chicken.actor:SetModelByCreatureDisplayID(TUNING.CHICKEN_DISPLAY);
|
||||
end
|
||||
chicken.life = TUNING.CHICKEN_SECONDS;
|
||||
chicken.peck = 0;
|
||||
chicken.move = 0;
|
||||
chicken.prey = nil;
|
||||
chicken.gone = nil;
|
||||
chicken.actor:SetPosition(hx + 1.5, hy, hz + 0.5);
|
||||
chicken.actor:Show();
|
||||
chicken.actor:SetMovementEnabled(true);
|
||||
chicken.actor:SetMovementSpeed("run", TUNING.CHICKEN_SPEED);
|
||||
tinsert(chickens, chicken);
|
||||
end
|
||||
|
||||
-- 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" then
|
||||
ThrowGrenade(item);
|
||||
elseif item.key == "swift" then
|
||||
BW.hero:PlaySpellVisual(item.spell, BW.hero, 0);
|
||||
BW.run.swiftUntil = BW.run.time + TUNING.SWIFTNESS_SECONDS;
|
||||
ApplyHeroSpeed();
|
||||
elseif item.key == "hearth" then
|
||||
BW.hero:PlaySpellVisual(item.spell, BW.hero, 0);
|
||||
for _, gem in ipairs(gems) do
|
||||
gem.pulled = true;
|
||||
end
|
||||
elseif item.key == "sapper" then
|
||||
Sapper(item, hx, hy);
|
||||
elseif item.key == "chicken" then
|
||||
SummonChicken(item, hx, hy, hz);
|
||||
end
|
||||
end
|
||||
|
||||
-- Whether a Battle Chicken holds this whelp: taunted, and the chicken out.
|
||||
local function Taunted(e)
|
||||
return e.taunter and not e.taunter.gone and BW.run.time < e.tauntUntil;
|
||||
end
|
||||
|
||||
local function UpdateItems(dt, hx, hy, hz)
|
||||
if BW.run.swiftUntil and BW.run.time >= BW.run.swiftUntil then
|
||||
BW.run.swiftUntil = nil;
|
||||
ApplyHeroSpeed();
|
||||
end
|
||||
local reach2 = TUNING.ITEM_REACH * TUNING.ITEM_REACH;
|
||||
local scale = BW.scene:GetEffectiveScale();
|
||||
local bob = math.sin(BW.run.time * 4) * 4;
|
||||
for i = table.getn(drops), 1, -1 do
|
||||
local drop = drops[i];
|
||||
drop.life = drop.life - dt;
|
||||
if Dist2(drop.x, drop.y, hx, hy) <= reach2 then
|
||||
local item = drop.item;
|
||||
ReleaseDrop(i);
|
||||
UseItem(item, hx, hy, hz);
|
||||
elseif drop.life <= 0 then
|
||||
ReleaseDrop(i);
|
||||
else
|
||||
local sx, sy = BW.scene:Project3DPointTo2D(drop.x, drop.y, drop.z + 1.2);
|
||||
if sx then
|
||||
drop.icon:ClearAllPoints();
|
||||
drop.icon:SetPoint("BOTTOM", BW.scene, "BOTTOMLEFT", sx / scale, sy / scale + bob);
|
||||
end
|
||||
end
|
||||
end
|
||||
UpdateChickens(dt, hx, hy, hz);
|
||||
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
|
||||
end
|
||||
|
||||
-- For the files that load after this one.
|
||||
BW.Taunted = Taunted
|
||||
BW.UpdateItems = UpdateItems
|
||||
BW.ClearItems = ClearItems
|
||||
@@ -0,0 +1,814 @@
|
||||
-- Broodwatch spells: targets, totems, ground areas, casts and leaps.
|
||||
|
||||
local _, BW = ...
|
||||
local CAST_SPACING = BW.CAST_SPACING
|
||||
local SPELLS = BW.SPELLS
|
||||
local PASSIVES = BW.PASSIVES
|
||||
local HOLY_NOVA_HEAL_TARGETS = BW.HOLY_NOVA_HEAL_TARGETS
|
||||
local enemies = BW.enemies
|
||||
local FloorAt = BW.FloorAt
|
||||
local Dist2 = BW.Dist2
|
||||
local Flash = BW.Flash
|
||||
local Damage = BW.Damage
|
||||
local Blast = BW.Blast
|
||||
local Hold = BW.Hold
|
||||
local Fear = BW.Fear
|
||||
local Weaken = BW.Weaken
|
||||
local TUNING = BW.TUNING
|
||||
local Mastered = BW.Mastered
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- The hero's spells
|
||||
|
||||
local function Stat(def, field, level)
|
||||
local v = def[field];
|
||||
v = v and v[level];
|
||||
if v and BW.run then
|
||||
if field == "radius" and BW.run.passives.amplify then
|
||||
v = v * (1 + PASSIVES.amplify.per * BW.run.passives.amplify / 100);
|
||||
elseif field == "count" and BW.run.passives.multishot then
|
||||
v = v + BW.run.passives.multishot;
|
||||
end
|
||||
end
|
||||
return v;
|
||||
end
|
||||
|
||||
-- Missile targets: up to `count` live enemies in range, by the spell's rule.
|
||||
local picked = {}
|
||||
local function PickTargets(def, count, hx, hy)
|
||||
for i = table.getn(picked), 1, -1 do
|
||||
picked[i] = nil;
|
||||
end
|
||||
local range2 = def.range * def.range;
|
||||
for _ = 1, count do
|
||||
local best, bestScore;
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive and not e.marked and not (def.kind == "dot" and e.dotLeft) then
|
||||
local x, y = e.actor:GetPosition();
|
||||
local d2 = Dist2(x, y, hx, hy);
|
||||
if d2 <= range2 then
|
||||
local score;
|
||||
if def.pick == "nearest" then
|
||||
score = -d2;
|
||||
elseif def.pick == "strongest" then
|
||||
score = e.hp;
|
||||
else
|
||||
score = math.random();
|
||||
end
|
||||
if not bestScore or score > bestScore then
|
||||
best, bestScore = e, score;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if not best then
|
||||
break;
|
||||
end
|
||||
best.marked = true;
|
||||
tinsert(picked, best);
|
||||
end
|
||||
for _, e in ipairs(picked) do
|
||||
e.marked = nil;
|
||||
end
|
||||
return picked;
|
||||
end
|
||||
|
||||
-- The live enemy nearest to (x, y) within `range` that `skip` allows.
|
||||
local function Nearest(x, y, range, skip)
|
||||
local best, bestD2 = nil, range * range;
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive and not skip(e) then
|
||||
local ex, ey = e.actor:GetPosition();
|
||||
local d2 = Dist2(ex, ey, x, y);
|
||||
if d2 <= bestD2 then
|
||||
best, bestD2 = e, d2;
|
||||
end
|
||||
end
|
||||
end
|
||||
return best;
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Magma Totems: each stands where it was planted and pulses until its time
|
||||
-- runs out. A pulse burns every enemy in its radius and shows the impact on
|
||||
-- up to MAGMA_IMPACTS of them.
|
||||
|
||||
local totems = {}
|
||||
local totemPool = {}
|
||||
local MAGMA_IMPACTS = 6
|
||||
|
||||
local function PlantTotems(def, level)
|
||||
local hx, hy, hz = BW.hero:GetPosition();
|
||||
local n = Stat(def, "totems", level);
|
||||
for k = 1, n do
|
||||
local totem = table.remove(totemPool);
|
||||
if not totem then
|
||||
totem = { actor = BW.scene:AcquireActor() };
|
||||
totem.actor:SetModelByCreatureDisplayID(def.display);
|
||||
totem.actor:HookScript("OnSpellVisualImpact", function(_, _, _, castID) BW.OnImpact(nil, nil, nil, castID); end);
|
||||
end
|
||||
local a = (k - 1) * 2 * math.pi / n + math.random() * 0.5;
|
||||
local x, y = hx + 2 * math.cos(a), hy + 2 * math.sin(a);
|
||||
totem.x, totem.y = x, y;
|
||||
totem.life = Stat(def, "duration", level);
|
||||
totem.damage = Stat(def, "damage", level);
|
||||
totem.radius = Stat(def, "radius", level);
|
||||
totem.pulseGap = 0.5;
|
||||
totem.actor:SetPosition(x, y, FloorAt(x, y, hz));
|
||||
totem.actor:Show();
|
||||
tinsert(totems, totem);
|
||||
end
|
||||
end
|
||||
|
||||
local function UpdateTotems(dt)
|
||||
local def = SPELLS.totem;
|
||||
for i = table.getn(totems), 1, -1 do
|
||||
local totem = totems[i];
|
||||
totem.life = totem.life - dt;
|
||||
if totem.life <= 0 then
|
||||
if Mastered("totem") then
|
||||
-- The eruption: every enemy in reach, at the mastery's multiple.
|
||||
local r2 = totem.radius * totem.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 shown < MAGMA_IMPACTS then
|
||||
shown = shown + 1;
|
||||
totem.actor:PlaySpellVisual(def.pulse, e.actor, 0);
|
||||
end
|
||||
Damage(e, totem.damage * def.mastery.burst, def.color);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
totem.actor:Hide();
|
||||
tinsert(totemPool, totem);
|
||||
table.remove(totems, i);
|
||||
else
|
||||
totem.pulseGap = totem.pulseGap - dt;
|
||||
if totem.pulseGap <= 0 then
|
||||
totem.pulseGap = totem.pulseGap + def.pulseEvery;
|
||||
local r2 = totem.radius * totem.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 shown < MAGMA_IMPACTS then
|
||||
shown = shown + 1;
|
||||
totem.actor:PlaySpellVisual(def.pulse, e.actor, 0);
|
||||
end
|
||||
Damage(e, totem.damage, def.color);
|
||||
end
|
||||
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
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Ground areas: each stands where it landed for its duration and burns every
|
||||
-- enemy inside it once a second.
|
||||
|
||||
local areas = {}
|
||||
local areaPool = {}
|
||||
|
||||
-- Slows an enemy to `factor` of its run speed until a little after the next
|
||||
-- tick; the step lets it go when that time passes.
|
||||
local SLOW_SECONDS = 1.2
|
||||
local function Slow(e, factor, seconds)
|
||||
e.slowUntil = math.max(e.slowUntil or 0, BW.run.time + (seconds or SLOW_SECONDS));
|
||||
if e.slowFactor ~= factor then
|
||||
e.slowFactor = factor;
|
||||
e.actor:SetMovementSpeed("run", e.kind.speed * factor);
|
||||
end
|
||||
end
|
||||
|
||||
local function Unslow(e)
|
||||
e.slowUntil = nil;
|
||||
e.slowFactor = nil;
|
||||
e.actor:SetMovementSpeed("run", e.kind.speed);
|
||||
end
|
||||
|
||||
-- The point of the thickest pack in range: of up to 24 enemies in range,
|
||||
-- the one with the most others within `radius` of it.
|
||||
local function PackCenter(range, radius)
|
||||
local hx, hy = BW.hero:GetPosition();
|
||||
local range2, r2 = range * range, radius * radius;
|
||||
local best, bestCount = nil, -1;
|
||||
local tried = 0;
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
if Dist2(x, y, hx, hy) <= range2 then
|
||||
local count = 0;
|
||||
for _, o in ipairs(enemies) do
|
||||
if o.alive then
|
||||
local ox, oy = o.actor:GetPosition();
|
||||
if Dist2(ox, oy, x, y) <= r2 then
|
||||
count = count + 1;
|
||||
end
|
||||
end
|
||||
end
|
||||
if count > bestCount then
|
||||
best, bestCount = e, count;
|
||||
end
|
||||
tried = tried + 1;
|
||||
if tried >= 24 then
|
||||
break;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if not best then
|
||||
return nil;
|
||||
end
|
||||
local x, y, z = best.actor:GetPosition();
|
||||
return x, y, z, best;
|
||||
end
|
||||
|
||||
local function PlantArea(key, level, x, y, z)
|
||||
local def = SPELLS[key];
|
||||
local area = table.remove(areaPool);
|
||||
if not area then
|
||||
area = { actor = BW.scene:AcquireActor() };
|
||||
end
|
||||
area.key = key;
|
||||
area.x, area.y = x, y;
|
||||
area.radius = Stat(def, "radius", level);
|
||||
area.damage = Stat(def, "damage", level);
|
||||
area.life = Stat(def, "duration", level);
|
||||
area.slow = Stat(def, "slow", level);
|
||||
area.tick = 1;
|
||||
area.actor:SetModelBySpellArea(def.spell, area.radius);
|
||||
area.actor:SetPosition(x, y, FloorAt(x, y, z));
|
||||
area.actor:Show();
|
||||
tinsert(areas, area);
|
||||
end
|
||||
|
||||
local function UpdateAreas(dt)
|
||||
local follow = Mastered("consecration");
|
||||
for i = table.getn(areas), 1, -1 do
|
||||
local area = areas[i];
|
||||
if follow and area.key == "consecration" then
|
||||
local hx, hy, hz = BW.hero:GetPosition();
|
||||
area.x, area.y = hx, hy;
|
||||
area.actor:SetPosition(hx, hy, hz);
|
||||
end
|
||||
area.life = area.life - dt;
|
||||
area.tick = area.tick - dt;
|
||||
if area.tick <= 0 then
|
||||
area.tick = area.tick + 1;
|
||||
local r2 = area.radius * area.radius;
|
||||
local color = SPELLS[area.key].color;
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
if Dist2(x, y, area.x, area.y) <= r2 then
|
||||
if area.slow then
|
||||
Slow(e, area.slow);
|
||||
end
|
||||
Damage(e, area.damage, color);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if area.life <= 0 then
|
||||
if area.key == "blizzard" and Mastered("blizzard") then
|
||||
local def = SPELLS.blizzard;
|
||||
local r2 = area.radius * area.radius;
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
if Dist2(x, y, area.x, area.y) <= r2 then
|
||||
Hold(e, def.mastery.freeze, SPELLS.frostnova.kit, true);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
area.actor:Hide();
|
||||
tinsert(areaPool, area);
|
||||
table.remove(areas, i);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- A storm aimed at a pack marks the ground with the reticle first
|
||||
-- (actor:SetGroundReticle on an actor with no model), then lands.
|
||||
local LANDING_SECONDS = 0.6
|
||||
local markerPool = {}
|
||||
|
||||
local function QueueLanding(key, level, x, y, z)
|
||||
local marker = table.remove(markerPool);
|
||||
if not marker then
|
||||
marker = { actor = BW.scene:AcquireActor() };
|
||||
end
|
||||
local floorZ = FloorAt(x, y, z);
|
||||
marker.actor:SetPosition(x, y, floorZ);
|
||||
marker.actor:SetGroundReticle(Stat(SPELLS[key], "radius", level));
|
||||
marker.actor:Show();
|
||||
tinsert(BW.run.landings, { marker = marker, key = key, level = level, x = x, y = y, z = floorZ, left = LANDING_SECONDS });
|
||||
end
|
||||
|
||||
local function ReleaseMarker(marker)
|
||||
marker.actor:ClearGroundReticle();
|
||||
marker.actor:Hide();
|
||||
tinsert(markerPool, marker);
|
||||
end
|
||||
|
||||
local function UpdateLandings(dt)
|
||||
for i = table.getn(BW.run.landings), 1, -1 do
|
||||
local landing = BW.run.landings[i];
|
||||
landing.left = landing.left - dt;
|
||||
if landing.left <= 0 then
|
||||
ReleaseMarker(landing.marker);
|
||||
PlantArea(landing.key, landing.level, landing.x, landing.y, landing.z);
|
||||
table.remove(BW.run.landings, i);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function ClearLandings()
|
||||
for i = table.getn(BW.run.landings), 1, -1 do
|
||||
ReleaseMarker(BW.run.landings[i].marker);
|
||||
BW.run.landings[i] = nil;
|
||||
end
|
||||
end
|
||||
|
||||
local function ClearAreas()
|
||||
for i = table.getn(areas), 1, -1 do
|
||||
areas[i].actor:Hide();
|
||||
tinsert(areaPool, areas[i]);
|
||||
areas[i] = nil;
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Stoneclaw Totems: each pulls the brood around it onto itself (the Battle
|
||||
-- Chicken's taunt, BW.Taunted) until it crumbles.
|
||||
|
||||
local wards = {}
|
||||
local wardPool = {}
|
||||
local WARD_PULSE = 0.5 -- seconds between two taunts
|
||||
|
||||
local function PlantWard(def, level)
|
||||
local hx, hy, hz = BW.hero:GetPosition();
|
||||
local ward = table.remove(wardPool);
|
||||
if not ward then
|
||||
ward = { actor = BW.scene:AcquireActor() };
|
||||
ward.actor:SetModelByCreatureDisplayID(def.display);
|
||||
end
|
||||
local a = math.random() * 2 * math.pi;
|
||||
local x, y = hx + 2.5 * math.cos(a), hy + 2.5 * math.sin(a);
|
||||
ward.x, ward.y = x, y;
|
||||
ward.life = Stat(def, "duration", level);
|
||||
ward.radius = Stat(def, "radius", level);
|
||||
ward.pulse = 0;
|
||||
ward.gone = nil;
|
||||
ward.actor:SetPosition(x, y, FloorAt(x, y, hz));
|
||||
ward.actor:Show();
|
||||
tinsert(wards, ward);
|
||||
end
|
||||
|
||||
-- `crumbled`: its time ran out (the mastery's stun), not a run's end.
|
||||
local function DropWard(i, crumbled)
|
||||
local ward = wards[i];
|
||||
if crumbled and Mastered("stoneclaw") then
|
||||
local r2 = ward.radius * ward.radius;
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
if Dist2(x, y, ward.x, ward.y) <= r2 then
|
||||
Hold(e, SPELLS.stoneclaw.mastery.stun);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
ward.gone = true;
|
||||
ward.actor:Hide();
|
||||
tinsert(wardPool, ward);
|
||||
table.remove(wards, i);
|
||||
end
|
||||
|
||||
local function UpdateWards(dt)
|
||||
for i = table.getn(wards), 1, -1 do
|
||||
local ward = wards[i];
|
||||
ward.life = ward.life - dt;
|
||||
if ward.life <= 0 then
|
||||
DropWard(i, true);
|
||||
else
|
||||
ward.pulse = ward.pulse - dt;
|
||||
if ward.pulse <= 0 then
|
||||
ward.pulse = ward.pulse + WARD_PULSE;
|
||||
local r2 = ward.radius * ward.radius;
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
if Dist2(x, y, ward.x, ward.y) <= r2 then
|
||||
e.taunter = ward;
|
||||
e.tauntUntil = BW.run.time + WARD_PULSE * 2;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function ClearWards()
|
||||
for i = table.getn(wards), 1, -1 do
|
||||
DropWard(i);
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- The imp: summoned the first time Summon Imp casts, it stays for the run,
|
||||
-- follows you and casts Firebolt (the spell's `missile`) at the nearest enemy.
|
||||
|
||||
local pet
|
||||
|
||||
local function SummonPet(def)
|
||||
if pet and pet.out then
|
||||
return pet;
|
||||
end
|
||||
if not pet then
|
||||
pet = { actor = BW.scene:AcquireActor() };
|
||||
pet.actor:SetModelByCreatureDisplayID(def.display);
|
||||
pet.actor:HookScript("OnSpellVisualImpact", function(_, _, _, castID) BW.OnImpact(nil, nil, nil, castID); end);
|
||||
end
|
||||
local hx, hy, hz = BW.hero:GetPosition();
|
||||
BW.hero:PlaySpellVisual(def.spell, BW.hero, 0);
|
||||
pet.actor:SetPosition(hx + 1.5, hy + 1.5, hz + 0.5);
|
||||
pet.actor:Show();
|
||||
pet.actor:SetMovementEnabled(true);
|
||||
pet.actor:SetMovementSpeed("run", TUNING.IMP_SPEED);
|
||||
pet.out = true;
|
||||
pet.move = 0;
|
||||
return pet;
|
||||
end
|
||||
|
||||
local function UpdatePet(dt, hx, hy, hz)
|
||||
if not (pet and pet.out) then
|
||||
return;
|
||||
end
|
||||
pet.move = pet.move - dt;
|
||||
if pet.move > 0 then
|
||||
return;
|
||||
end
|
||||
pet.move = 0.4;
|
||||
local px, py = pet.actor:GetPosition();
|
||||
if Dist2(px, py, hx, hy) > TUNING.IMP_FOLLOW * TUNING.IMP_FOLLOW then
|
||||
-- To your side and behind you, as a pet follows.
|
||||
local a = BW.run.heroYaw + 2.4;
|
||||
pet.actor:MoveTo(hx + 2 * math.cos(a), hy + 2 * math.sin(a), hz);
|
||||
end
|
||||
end
|
||||
|
||||
local function ClearPet()
|
||||
if pet and pet.out then
|
||||
pet.out = nil;
|
||||
pet.actor:SetMovementEnabled(false);
|
||||
pet.actor:Hide();
|
||||
end
|
||||
end
|
||||
|
||||
local petHit = {}
|
||||
local function NotPetHit(e)
|
||||
return petHit[e];
|
||||
end
|
||||
|
||||
local function PetCast(key, def, level)
|
||||
local imp = SummonPet(def);
|
||||
local px, py = imp.actor:GetPosition();
|
||||
local bolts = Mastered(key) and def.mastery.bolts or 1;
|
||||
local cast = false;
|
||||
for _ = 1, bolts do
|
||||
local target = Nearest(px, py, def.range, NotPetHit);
|
||||
if not target then
|
||||
break;
|
||||
end
|
||||
petHit[target] = true;
|
||||
local castID = imp.actor:PlaySpellVisual(def.missile, target.actor, 0);
|
||||
if castID then
|
||||
BW.run.inFlight[castID] = { key = key, target = target, damage = Stat(def, "damage", level) };
|
||||
end
|
||||
cast = true;
|
||||
end
|
||||
for e in pairs(petHit) do
|
||||
petHit[e] = nil;
|
||||
end
|
||||
return cast;
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Power Word: Shield: the bites soak into it first (the step), and its bubble
|
||||
-- shows until it breaks or its time ends.
|
||||
|
||||
-- `broken`: the bites used it up (the mastery's burst), not its time.
|
||||
local function EndShield(broken)
|
||||
if broken and Mastered("shield") then
|
||||
local m = SPELLS.shield.mastery;
|
||||
local hx, hy = BW.hero:GetPosition();
|
||||
local r2 = m.radius * m.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
|
||||
Damage(e, m.damage, SPELLS.shield.color);
|
||||
Blast(e, hx, hy, m.knock);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if BW.run.shield then
|
||||
BW.run.shield = nil;
|
||||
BW.run.shieldUntil = nil;
|
||||
BW.hero:SetSpellVisualKit();
|
||||
BW.UpdateHealth();
|
||||
end
|
||||
end
|
||||
|
||||
-- The hero casts one visual at a time; casts wait in a queue.
|
||||
local function QueueCast(entry)
|
||||
tinsert(BW.run.castQueue, entry);
|
||||
end
|
||||
|
||||
local function Fire(key, state)
|
||||
local def = SPELLS[key];
|
||||
local level = state.level;
|
||||
local hx, hy = BW.hero:GetPosition();
|
||||
if def.kind == "missile" or def.kind == "dot" or def.kind == "chain" then
|
||||
local targets = PickTargets(def, Stat(def, "count", level) or 1, hx, hy);
|
||||
if table.getn(targets) == 0 then
|
||||
return false;
|
||||
end
|
||||
for _, e in ipairs(targets) do
|
||||
QueueCast({
|
||||
key = key, state = state, target = e, damage = Stat(def, "damage", level),
|
||||
jumps = Stat(def, "jumps", level), level = level,
|
||||
});
|
||||
state.pending = state.pending + 1;
|
||||
end
|
||||
elseif def.kind == "pet" then
|
||||
return PetCast(key, def, level);
|
||||
elseif def.kind == "totem" or def.kind == "ward" or def.kind == "shield" or def.kind == "thorns" then
|
||||
QueueCast({ key = key, state = state, level = level });
|
||||
state.pending = state.pending + 1;
|
||||
elseif def.kind == "area" then
|
||||
local x, y, z;
|
||||
if def.place == "pack" then
|
||||
x, y, z = PackCenter(def.range, Stat(def, "radius", level));
|
||||
if not x then
|
||||
return false;
|
||||
end
|
||||
else
|
||||
x, y, z = BW.hero:GetPosition();
|
||||
end
|
||||
QueueCast({ key = key, state = state, level = level, x = x, y = y, z = z });
|
||||
state.pending = state.pending + 1;
|
||||
else
|
||||
QueueCast({
|
||||
key = key, state = state, level = level, damage = Stat(def, "damage", level),
|
||||
radius = Stat(def, "radius", level), heal = Stat(def, "heal", level),
|
||||
});
|
||||
state.pending = state.pending + 1;
|
||||
end
|
||||
return true;
|
||||
end
|
||||
|
||||
local function CastNext()
|
||||
local entry = table.remove(BW.run.castQueue, 1);
|
||||
if not entry then
|
||||
return;
|
||||
end
|
||||
entry.state.pending = entry.state.pending - 1;
|
||||
local def = SPELLS[entry.key];
|
||||
if entry.target then
|
||||
if not entry.target.alive then
|
||||
return CastNext();
|
||||
end
|
||||
local castID = BW.hero:PlaySpellVisual(def.spell, entry.target.actor, 0);
|
||||
if castID then
|
||||
BW.run.inFlight[castID] = entry;
|
||||
end
|
||||
elseif def.kind == "totem" then
|
||||
BW.hero:PlaySpellVisual(def.spell, BW.hero, 0);
|
||||
PlantTotems(def, entry.level);
|
||||
elseif def.kind == "ward" then
|
||||
BW.hero:PlaySpellVisual(def.spell, BW.hero, 0);
|
||||
PlantWard(def, entry.level);
|
||||
elseif def.kind == "shield" then
|
||||
BW.hero:PlaySpellVisual(def.spell, BW.hero, 0);
|
||||
BW.run.shield = BW.MaxHp() * Stat(def, "absorb", entry.level) / 100;
|
||||
BW.run.shieldUntil = BW.run.time + Stat(def, "duration", entry.level);
|
||||
BW.hero:SetSpellVisualKit(def.kit);
|
||||
BW.UpdateHealth();
|
||||
elseif def.kind == "thorns" then
|
||||
BW.hero:PlaySpellVisual(def.spell, BW.hero, 0);
|
||||
BW.run.thornsUntil = BW.run.time + Stat(def, "duration", entry.level);
|
||||
BW.run.thornsDamage = Stat(def, "damage", entry.level);
|
||||
BW.run.thornsTick = 1;
|
||||
elseif def.kind == "area" then
|
||||
BW.hero:PlaySpellVisual(def.spell, BW.hero, 0);
|
||||
if def.place == "pack" then
|
||||
QueueLanding(entry.key, entry.level, entry.x, entry.y, entry.z);
|
||||
if entry.key == "rainoffire" and Mastered("rainoffire") then
|
||||
local hx, hy = BW.hero:GetPosition();
|
||||
local other = PickTargets(def, 1, hx, hy)[1];
|
||||
if other then
|
||||
local x, y, z = other.actor:GetPosition();
|
||||
QueueLanding(entry.key, entry.level, x, y, z);
|
||||
end
|
||||
end
|
||||
else
|
||||
PlantArea(entry.key, entry.level, entry.x, entry.y, entry.z);
|
||||
end
|
||||
else
|
||||
BW.hero:PlaySpellVisual(def.spell, BW.hero, 0);
|
||||
local hx, hy = BW.hero:GetPosition();
|
||||
local r2 = entry.radius * entry.radius;
|
||||
local root = Stat(def, "root", entry.level);
|
||||
local fear = Stat(def, "fear", entry.level);
|
||||
local weaken = Stat(def, "weaken", entry.level);
|
||||
local mastery = Mastered(entry.key) and def.mastery;
|
||||
local hits = 0;
|
||||
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 entry.damage then
|
||||
Damage(e, entry.damage, def.color);
|
||||
end
|
||||
if e.alive then
|
||||
if root then
|
||||
Hold(e, root, def.kit);
|
||||
end
|
||||
if fear then
|
||||
Fear(e, fear, def.kit);
|
||||
end
|
||||
if weaken then
|
||||
Weaken(e, weaken, Stat(def, "duration", entry.level));
|
||||
end
|
||||
end
|
||||
if def.knock then
|
||||
Blast(e, hx, hy, def.knock);
|
||||
end
|
||||
if mastery then
|
||||
if mastery.slow and e.alive then
|
||||
Slow(e, mastery.slow, mastery.seconds);
|
||||
end
|
||||
if mastery.knock then
|
||||
Blast(e, hx, hy, mastery.knock);
|
||||
end
|
||||
end
|
||||
hits = hits + 1;
|
||||
end
|
||||
end
|
||||
end
|
||||
if entry.heal and hits > 0 then
|
||||
BW.run.pendingHeal = BW.run.pendingHeal + entry.heal * math.min(hits, HOLY_NOVA_HEAL_TARGETS);
|
||||
end
|
||||
-- Arcane Explosion's Echo bursts again (once); the shout's battle cry
|
||||
-- speeds you while it lasts.
|
||||
if mastery and entry.key == "nova" and not entry.echo then
|
||||
QueueCast({
|
||||
key = entry.key, state = entry.state, level = entry.level, damage = entry.damage,
|
||||
radius = entry.radius, echo = true,
|
||||
});
|
||||
entry.state.pending = entry.state.pending + 1;
|
||||
end
|
||||
if mastery and entry.key == "demoshout" then
|
||||
BW.run.shoutUntil = BW.run.time + Stat(def, "duration", entry.level);
|
||||
BW.ApplyHeroSpeed();
|
||||
end
|
||||
end
|
||||
BW.run.castGap = CAST_SPACING;
|
||||
end
|
||||
|
||||
-- Puts Corruption on an enemy: `damage` each second for `seconds`, and it
|
||||
-- spreads to `spread` neighbours when the enemy dies.
|
||||
local function Corrupt(e, damage, seconds, spread)
|
||||
e.dotLeft = seconds;
|
||||
e.dotTick = 1;
|
||||
e.dotDamage = damage;
|
||||
e.dotSeconds = seconds;
|
||||
e.dotSpread = spread;
|
||||
Flash(e);
|
||||
end
|
||||
|
||||
-- Chain Lightning leaps on from the enemy it hit: that enemy casts the next
|
||||
-- leap at its nearest neighbour not yet hit.
|
||||
local chainHit
|
||||
local function NotChainHit(e)
|
||||
return chainHit[e];
|
||||
end
|
||||
|
||||
local function Leap(entry)
|
||||
if not entry.jumps or entry.jumps <= 0 then
|
||||
return;
|
||||
end
|
||||
entry.hit = entry.hit or {};
|
||||
entry.hit[entry.target] = true;
|
||||
local from = entry.target;
|
||||
local fx, fy = from.actor:GetPosition();
|
||||
chainHit = entry.hit;
|
||||
local nextTarget = Nearest(fx, fy, SPELLS[entry.key].hop, NotChainHit);
|
||||
chainHit = nil;
|
||||
if not nextTarget then
|
||||
return;
|
||||
end
|
||||
local castID = from.actor:PlaySpellVisual(SPELLS[entry.key].spell, nextTarget.actor, 0);
|
||||
if castID then
|
||||
BW.run.inFlight[castID] = {
|
||||
key = entry.key, target = nextTarget, jumps = entry.jumps - 1, hit = entry.hit,
|
||||
damage = Mastered(entry.key) and entry.damage * SPELLS[entry.key].mastery.grow or entry.damage,
|
||||
};
|
||||
end
|
||||
end
|
||||
|
||||
BW.OnImpact = function(_, _, _, castID)
|
||||
if not BW.run then
|
||||
return;
|
||||
end
|
||||
local entry = BW.run.inFlight[castID];
|
||||
if not entry then
|
||||
return;
|
||||
end
|
||||
BW.run.inFlight[castID] = nil;
|
||||
if entry.onImpact then
|
||||
entry.onImpact(entry);
|
||||
return;
|
||||
end
|
||||
local def = SPELLS[entry.key];
|
||||
if def.kind == "dot" then
|
||||
if entry.target.alive then
|
||||
Corrupt(entry.target, entry.damage, Stat(def, "duration", entry.level), Stat(def, "spread", entry.level));
|
||||
end
|
||||
return;
|
||||
end
|
||||
if def.kind == "chain" then
|
||||
Leap(entry);
|
||||
end
|
||||
local target = entry.target;
|
||||
local wasAlive = target.alive;
|
||||
Damage(target, entry.damage, def.color);
|
||||
if not Mastered(entry.key) or not def.mastery then
|
||||
return;
|
||||
end
|
||||
local m = def.mastery;
|
||||
if entry.key == "frostbolt" and target.alive then
|
||||
Hold(target, m.freeze, SPELLS.frostnova.kit, true);
|
||||
elseif entry.key == "shadowbolt" and wasAlive and not target.alive then
|
||||
local tx, ty = target.actor:GetPosition();
|
||||
local r2 = m.radius * m.radius;
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
if Dist2(x, y, tx, ty) <= r2 then
|
||||
Damage(e, entry.damage * m.share, def.color);
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif entry.key == "lightning" and not entry.overload and math.random() < m.chance then
|
||||
local hx, hy = BW.hero:GetPosition();
|
||||
local other = PickTargets(def, 1, hx, hy)[1];
|
||||
local state = BW.run.spells[entry.key];
|
||||
if other and state then
|
||||
QueueCast({ key = entry.key, state = state, target = other, damage = entry.damage, level = entry.level, overload = true });
|
||||
state.pending = state.pending + 1;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- For the files that load after this one.
|
||||
BW.UpdateWards = UpdateWards
|
||||
BW.ClearWards = ClearWards
|
||||
BW.UpdatePet = UpdatePet
|
||||
BW.ClearPet = ClearPet
|
||||
BW.EndShield = EndShield
|
||||
BW.Stat = Stat
|
||||
BW.Nearest = Nearest
|
||||
BW.UpdateTotems = UpdateTotems
|
||||
BW.ClearTotems = ClearTotems
|
||||
BW.Unslow = Unslow
|
||||
BW.PackCenter = PackCenter
|
||||
BW.UpdateAreas = UpdateAreas
|
||||
BW.markerPool = markerPool
|
||||
BW.ReleaseMarker = ReleaseMarker
|
||||
BW.UpdateLandings = UpdateLandings
|
||||
BW.ClearLandings = ClearLandings
|
||||
BW.ClearAreas = ClearAreas
|
||||
BW.Fire = Fire
|
||||
BW.CastNext = CastNext
|
||||
BW.Corrupt = Corrupt
|
||||
+9
-7
@@ -76,13 +76,14 @@ endif()
|
||||
add_library(${PROJECT_NAME} SHARED
|
||||
${SOURCES} ${HEADERS} ${TINYCBOR_SOURCES})
|
||||
|
||||
# Embed the addons under `src/embed/AddOns/` and `src/bindings/Bindings.xml` as RCDATA
|
||||
# resources. The file-read hook in `src/addons/Embedded.cpp` serves the
|
||||
# addon files when the engine asks for paths under
|
||||
# `Interface\AddOns\<addon>\`, so the addons load even when the user
|
||||
# doesn't have them on disk. `src/bindings/Inject.cpp` splices the bindings
|
||||
# XML (our TARGETING-group additions) into the engine's
|
||||
# `Interface\FrameXML\Bindings.xml` at load time.
|
||||
# Embed the addons under `src/embed/AddOns/`, `src/bindings/Bindings.xml` and
|
||||
# the textures under `src/embed/Textures/` as RCDATA resources. The file-read
|
||||
# hook in `src/addons/Embedded.cpp` serves the addon files when the engine
|
||||
# asks for paths under `Interface\AddOns\<addon>\`, so the addons load even
|
||||
# when the user doesn't have them on disk. `src/bindings/Inject.cpp` splices
|
||||
# the bindings XML (our TARGETING-group additions) into the engine's
|
||||
# `Interface\FrameXML\Bindings.xml` at load time. `src/texture/Builtin.cpp`
|
||||
# serves each texture at its game path when no file has it.
|
||||
include(cmake/embed_resources.cmake)
|
||||
embed_resources(
|
||||
TARGET ${PROJECT_NAME}
|
||||
@@ -90,6 +91,7 @@ embed_resources(
|
||||
ADDONS "!!!ClassicAPI" "ClassicAPI_DebugTools" "ClassicAPI_EventTrace"
|
||||
ADDON_VERSION "${CLASSICAPI_TOC_VERSION}"
|
||||
BINDINGS_XML "${CMAKE_CURRENT_SOURCE_DIR}/src/bindings/Bindings.xml"
|
||||
TEXTURES_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/src/embed/Textures"
|
||||
OUT_DIR "${CMAKE_BINARY_DIR}/generated")
|
||||
|
||||
target_compile_definitions(
|
||||
|
||||
@@ -22,6 +22,7 @@ reference is in **[docs/API.md](docs/API.md)**.
|
||||
| Inline textures | Draws inline texture markup (`\|T…\|t`) and atlas markup (`\|A…\|a`) as icons in FontStrings, chat, and tooltips. This covers item and spell icons, raid-target markers, and the coin icons in money strings. `GetStringWidth` and `GetStringHeight` count the icons, so measured width and text wrapping stay correct. Done in pure C++ by hooking the engine's text pipeline — no addon. |
|
||||
| [Soft targeting](docs/API.md#soft-interact-softinteract--anyinteract) | The Interact Key and soft targets. Bind **Interact With Target** to a key to interact with the nearest NPC, corpse, mailbox, chest or gathering node in front of you. The [`softenemy` and `softfriend`](docs/API.md#soft-enemy-and-soft-friend-softenemy--softfriend--anyenemy--anyfriend) tokens point at the nearest enemy and friendly unit without a change of target. With no target, harmful spells go to `softenemy` and helpful spells go to `softfriend`. `PLAYER_SOFT_*_CHANGED` events fire when the units change. |
|
||||
| [Animations](docs/API.md#animation) | Animation groups on frames, textures and font strings, made in Lua or XML: `CreateAnimationGroup`, looping, smoothing, scripts and templates, with `Alpha`, `Translation`, `Scale`, `Rotation` and `Path` applied. |
|
||||
| [Model scenes](docs/API.md#modelscene) | The `ModelScene` frame type shows many models in one 3D scene, with its own camera, light and fog. Each model is an actor that you place, turn, scale and animate. An actor shows a creature display, a model file or a copy of a unit. It can also seat a rider, show spell visual kits and pause on one frame. A scene can show a WMO, for example a dungeon, around the actors, with its props and their sounds. An actor can move on the WMO as a character moves in the world: it runs, jumps, falls, swims and walks to a point. You hear the sounds of a scene from its camera, or from an actor. A follow camera follows an actor as the camera of the world follows your character, and stops in front of walls. An actor can glow, show a selection circle and show a name over its head, and a scene can show the click-to-move marker. |
|
||||
| Tooltip line cap | Removes `GameTooltip`'s 30-line limit for every `GameTooltipTemplate` frame (`GameTooltip`, `ShoppingTooltip1/2`, `ItemRefTooltip`, AtlasLoot, …). A tooltip adds a new line pair when it needs one, so long tooltips and comparison blocks (for example, pfUI's eqcompare) keep all their lines. |
|
||||
| [XML attributes](docs/API.md#parentkey-in-xml) | More XML attributes work: `parentKey="Name"` makes `parent.Name` point to a frame, texture or font string, `<OnLoad function="Name"/>` uses a global function as a script handler, `<OnLoad method="Name"/>` uses the frame's own function, `mixin="A, B"` copies mixins onto the object, `<KeyValues>` sets fields on the object, a button's `<NormalFont style="..."/>` sets its font, and `inherits="A, B"` applies more than one template. `parentArray="Name"` adds the object to a list on its parent. An `<Anchor>` takes `x` and `y` offsets and `relativeKey="$parent.Key"`. `<OnEnter inherit="prepend">` runs the template's handler and then its own, and `inherit="append"` runs them in the other order. 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`. |
|
||||
@@ -88,6 +89,8 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
|
||||
| [Frame: StatusBar](docs/API.md#statusbarsetreversefillisreversefill--statusbargetreversefill) | `:SetReverseFill` / `:GetReverseFill`, `:SetRotatesTexture` / `:GetRotatesTexture`, `:SetFillStyle` / `:GetFillStyle`, `:SetColorFill`, `:SetStatusBarDesaturated` / `:IsStatusBarDesaturated`, `:SetStatusBarDesaturation` / `:GetStatusBarDesaturation`, `:SetTimerDuration` / `:GetTimerDuration`, `:SetValue` / `:SetMinMaxValues` with `interpolation`, `:GetInterpolatedValue`, `:IsInterpolating`, `:SetToTargetValue` |
|
||||
| [Frame: Slider](docs/API.md#sliderenable--sliderdisable--sliderisenabled) | `:Enable` / `:Disable` / `:IsEnabled` |
|
||||
| [Frame: Cooldown](docs/API.md#createframecooldown--name-parent-template) | `CreateFrame("Cooldown")`, `:SetCooldown`, `:SetCooldownDuration`, `:SetCooldownFromExpirationTime`, `:Clear`, `:GetCooldownTimes`, `:GetCooldownDuration`, `:GetCooldownDisplayDuration`, `:Pause` / `:Resume`, `:SetPaused` / `:IsPaused`, `:SetReverse` / `:GetReverse`, `:SetDrawEdge` / `:GetDrawEdge`, `:SetDrawSwipe` / `:GetDrawSwipe`, `:SetDrawBling` / `:GetDrawBling`, `:SetSwipeColor`, `:SetEdgeColor`, `:SetSwipeTexture`, `:SetEdgeTexture`, `:SetBlingTexture`, `:SetRotation` / `:GetRotation`, `:SetHideCountdownNumbers` / `:GetHideCountdownNumbers`, `:GetCountdownFontString`, `:SetCountdownAbbrevThreshold` / `:GetCountdownAbbrevThreshold`, `:SetCountdownMillisecondsThreshold` / `:GetCountdownMillisecondsThreshold`, `:SetMinimumCountdownDuration` / `:GetMinimumCountdownDuration`, `OnCooldownDone`, `CooldownFrame_Set`, `CooldownFrame_Clear`, `CooldownFrame_SetDisplayAsPercentage` |
|
||||
| [Frame: ModelScene](docs/API.md#modelscene) | `CreateFrame("ModelScene")`, `<ModelScene>` (`drawLayer`, `fogNear`, `fogFar`, `<FogColor>`, `<ViewInsets>`), `:CreateActor`, `:GetNumActors`, `:GetActorAtIndex`, `:SetCameraPosition` / `:GetCameraPosition`, `:SetCameraOrientationByYawPitchRoll`, `:SetCameraOrientationByAxisVectors`, `:GetCameraForward`, `:GetCameraRight`, `:GetCameraUp`, `:SetCameraFieldOfView` / `:GetCameraFieldOfView`, `:SetCameraNearClip` / `:GetCameraNearClip`, `:SetCameraFarClip` / `:GetCameraFarClip`, `:SetViewInsets` / `:GetViewInsets`, `:SetViewTranslation` / `:GetViewTranslation`, `:Project3DPointTo2D`, `"FollowCamera"` (`:SetFollowActor` / `:GetFollowActor`), `:SetLightType` / `:GetLightType`, `:SetLightVisible` / `:IsLightVisible`, `:SetLightDirection` / `:GetLightDirection`, `:SetLightPosition` / `:GetLightPosition`, `:SetLightAmbientColor` / `:GetLightAmbientColor`, `:SetLightDiffuseColor` / `:GetLightDiffuseColor`, `:SetFogColor` / `:GetFogColor`, `:SetFogNear` / `:GetFogNear`, `:SetFogFar` / `:GetFogFar`, `:ClearFog`, `:SetDrawLayer` / `:GetDrawLayer`, `:SetPaused`, `:GetSceneTime`, `:After`, `:NewTimer`, `:NewTicker`, `:SetAllowOverlappedModels` / `:GetAllowOverlappedModels`, `:SetGroundTerrain` / `:GetGroundTerrain`, `:SetWorldModel`, `:SetWorldModelByMapID`, `:ClearWorldModel`, `: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 +99,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` |
|
||||
@@ -106,13 +109,15 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
|
||||
| [Loot](docs/API.md#loot) | `C_Loot.GetNearbyLootableUnits`, `C_Loot.GetLastScanResults`, `C_Loot.IsScanInProgress`, `C_Loot.LootAllCorpses`, `C_Loot.LootUnit`, `C_Loot.LootUnitItem`, `C_Loot.ScanNearbyLoot` |
|
||||
| [LootHistory](docs/API.md#loothistory) | `C_LootHistory.GetNumItems`, `C_LootHistory.GetItem`, `C_LootHistory.GetPlayerInfo`, `C_LootHistory.Clear` |
|
||||
| [LossOfControl](docs/API.md#lossofcontrol) | `C_LossOfControl.GetActiveLossOfControlData`, `C_LossOfControl.GetActiveLossOfControlDataCount`, `C_LossOfControl.GetSchoolLockout` |
|
||||
| [Lua](docs/API.md#lua) | `collectgarbage` (5.1 options), `coroutine.create`, `coroutine.resume`, `coroutine.running`, `coroutine.status`, `coroutine.wrap`, `coroutine.yield`, `CreateFromMixins`, `GetCallstackHeight`, `GetErrorCallstackHeight`, `math.fmod`, `math.huge`, `math.modf`, `Mixin`, `select`, `SetErrorCallstackHeight`, `strcmputf8i`, `strconcat`, `string.concat`, `string.gmatch`, `string.gsub` (table replacement), `string.join`, `string.match`, `string.reverse`, `string.split`, `string.trim`, `strjoin`, `strlenutf8`, `strmatch`, `strreplace`, `strrev`, `strsplit`, `strsplittable`, `strtrim`, `table.count`, `table.maxn`, `table.wipe`, `unpack` (range args), `wipe`, `xpcall` (argument forwarding) |
|
||||
| [Lua](docs/API.md#lua) | `collectgarbage` (5.1 options), `coroutine.create`, `coroutine.resume`, `coroutine.running`, `coroutine.status`, `coroutine.wrap`, `coroutine.yield`, `CreateFromMixins`, `debuglocals`, `debugstack` (coroutine argument), `difftime`, `GetCallstackHeight`, `GetErrorCallstackHeight`, `gmatch`, `math.cosh`, `math.fmod`, `math.huge`, `math.modf`, `math.sinh`, `math.tanh`, `Mixin`, `newproxy`, `scrub`, `select`, `SetErrorCallstackHeight`, `strbyte` (range args), `strcmputf8i`, `strconcat`, `string.byte` (range args), `string.concat`, `string.format` (two-digit `%n$`), `string.gmatch`, `string.gsub` (table replacement, `%0`), `string.join`, `string.match`, `string.reverse`, `string.split`, `string.trim`, `strjoin`, `strlenutf8`, `strmatch`, `strreplace`, `strrev`, `strsplit`, `strsplittable`, `strtrim`, `table.count`, `table.maxn`, `table.removemulti`, `table.wipe`, `unpack` (range args), `wipe`, `xpcall` (argument forwarding) |
|
||||
| [Macros](docs/API.md#macros) | `C_Macro.CreateMacro`, `C_Macro.EditMacro`, `C_Macro.SetMacroDisplay`, `DeleteMacro` / `EditMacro` / `GetMacroInfo` / `PickupMacro` by name, `GetLooseMacroIcons`, `GetLooseMacroItemIcons`, `GetMacroBody`, `GetMacroIcons`, `GetMacroItem`, `GetMacroItemIcons`, `GetMacroSpell`, `GetRunningMacro`, `GetRunningMacroButton`, `RunMacroText`, `SetMacroItem`, `SetMacroSpell`, `StopMacro` |
|
||||
| [Mail](docs/API.md#mail) | `GetInboxItemLink`, `GetSendMailItemLink` |
|
||||
| [Map](docs/API.md#map) | `C_Map.CanSetUserWaypointOnMap`, `C_Map.ClearUserWaypoint`, `C_Map.GetAreaInfo`, `C_Map.GetAreas`, `C_Map.GetAreaTriggerInfo`, `C_Map.GetAreaTriggers`, `C_Map.GetBestMapForUnit`, `C_Map.GetFallbackWorldMapID`, `C_Map.GetMapAreaIDs`, `C_Map.GetMapArtLayers`, `C_Map.GetMapArtLayerTextures`, `C_Map.GetMapChildrenInfo`, `C_Map.GetMapInfo`, `C_Map.GetMapInfoAtPosition`, `C_Map.GetMapOverlays`, `C_Map.GetMapPosFromWorldPos`, `C_Map.GetMapRectOnMap`, `C_Map.GetMapWorldSize`, `C_Map.GetPlayerMapPosition`, `C_Map.GetUserWaypoint`, `C_Map.GetUserWaypointFromHyperlink`, `C_Map.GetUserWaypointHyperlink`, `C_Map.GetUserWaypointPositionForMap`, `C_Map.GetWorldPosFromMapPos`, `C_Map.HasUserWaypoint`, `C_Map.MapHasArt`, `C_Map.SetUserWaypoint` |
|
||||
| [MapExplorationInfo](docs/API.md#mapexplorationinfo) | `C_MapExplorationInfo.GetExploredAreaIDsAtPosition`, `C_MapExplorationInfo.GetExploredMapTextures`, `C_MapExplorationInfo.GetUnexploredMapTextures` |
|
||||
| [MerchantFrame](docs/API.md#merchantframe) | `C_MerchantFrame.GetBuybackItemID`, `C_MerchantFrame.GetItemInfo`, `C_MerchantFrame.GetNumJunkItems`, `C_MerchantFrame.IsMerchantItemRefundable`, `C_MerchantFrame.IsSellAllJunkEnabled`, `C_MerchantFrame.SellAllJunkItems`, `GetBuybackItemLink` |
|
||||
| [Model](docs/API.md#model) | `DressUpModel:TryOn` (links, recipes, names), `DressUpModel:UndressSlot`, `Model:SetGlow`, `PlayerModel:RefreshCamera`, `PlayerModel:SetCamDistanceScale`, `PlayerModel:SetCreature`, `PlayerModel:SetDisplayInfo`, `PlayerModel:SetPortraitZoom`, `PlayerModel:SetRotation` (`animate`) |
|
||||
| [Model](docs/API.md#model) | `DressUpModel:TryOn` (links, recipes, names, hand, enchant glow, result), `DressUpModel:UndressSlot`, `DressUpModel:IsGeoReady`, `DressUpModel:SetSheathed` / `GetSheathed`, `DressUpModel:SetAutoDress` / `GetAutoDress`, `DressUpModel:GetItemTransmogInfo` / `GetItemTransmogInfoList` / `SetItemTransmogInfo`, `DressUpModel:IsSlotAllowed` / `IsSlotVisible`, `Model:SetGlow`, `PlayerModel:RefreshCamera`, `PlayerModel:SetCamDistanceScale`, `PlayerModel:SetCreature` (`displayID`), `PlayerModel:SetDisplayInfo`, `PlayerModel:SetItem`, `PlayerModel:SetPortraitZoom`, `PlayerModel:SetRotation` (`animate`), `PlayerModel:SetUnit` (result), `PlayerModel:CanSetUnit`, `PlayerModel:GetDisplayInfo`, `PlayerModel:SetAnimation`, `PlayerModel:FreezeAnimation`, `PlayerModel:HasAnimation`, `PlayerModel:SetDoBlend` / `GetDoBlend`, `PlayerModel:SetKeepModelOnHide` / `GetKeepModelOnHide`, `PlayerModel:ApplySpellVisualKit` |
|
||||
| [ModelInfo](docs/API.md#c_modelinfogetmodelsceneinfobyidmodelsceneid) | `C_ModelInfo.GetModelSceneInfoByID`, `C_ModelInfo.GetModelSceneCameraInfoByID`, `C_ModelInfo.GetModelSceneActorInfoByID`, `C_ModelInfo.GetModelSceneActorDisplayInfoByID` |
|
||||
| [Mount](docs/API.md#mount) | `C_MountJournal.GetMountFromItem`, `C_MountJournal.GetMountFromSpell`, `C_MountJournal.GetMountInfoExtraByID` |
|
||||
| [NamePlate](docs/API.md#nameplate) | `C_NamePlate.GetNamePlateForGUID`, `C_NamePlate.GetNamePlateForUnit`, `C_NamePlate.GetNamePlateGUIDs`, `C_NamePlate.GetNamePlates` |
|
||||
| [NameCache](docs/API.md#namecache) | `C_PlayerCache.GetPlayerInfoByName`, `C_PlayerCache.IsEnabled`, `C_PlayerCache.RememberPlayer`, `C_PlayerCache.SetEnabled`, `GetPlayerInfoByGUID`, `UnitNameFromGUID` |
|
||||
| [NewItems](docs/API.md#newitems) | `C_NewItems.ClearAll`, `C_NewItems.IsNewItem`, `C_NewItems.RemoveNewItem` |
|
||||
@@ -131,7 +136,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
|
||||
| [Talent](docs/API.md#talent) | `GetTalentIDByIndex`, `GetTalentSpellID` |
|
||||
| [Targeting](docs/API.md#targeting) | `GetPlayerFacing`, `TargetDirectionEnemy`, `TargetDirectionFriend`, `TargetDirectionFinished`, `TargetNearest`, `TargetNearestEnemyPlayer`, `TargetNearestFriendPlayer`, `InteractUnit`, `C_PlayerInteractionManager.InteractUnit`, `IsTargetLoose` |
|
||||
| [TaxiMap](docs/API.md#taximap) | `C_TaxiMap.GetTaxiNodesForMap`, `C_TaxiMap.GetAllTaxiNodes`, `C_TaxiMap.GetTaxiPaths`, `C_TaxiMap.GetTaxiPathWaypoints`, `C_TaxiMap.GetTaxiRoute` |
|
||||
| [Texture](docs/API.md#texture) | `C_Texture.GetAtlasElementID`, `C_Texture.GetAtlasElements`, `C_Texture.GetAtlasExists`, `C_Texture.GetAtlasID`, `C_Texture.GetAtlasInfo`, `C_Texture.RegisterAtlas` |
|
||||
| [Texture](docs/API.md#texture) | `C_Texture.GetAtlasElementID`, `C_Texture.GetAtlasElements`, `C_Texture.GetAtlasExists`, `C_Texture.GetAtlasID`, `C_Texture.GetAtlasInfo`, `C_Texture.RegisterAtlas`, `SetPortraitTextureFromCreatureDisplayID` |
|
||||
| [Time](docs/API.md#time) | `C_DateAndTime.AdjustTimeByDays`, `C_DateAndTime.AdjustTimeByMinutes`, `C_DateAndTime.CompareCalendarTime`, `C_DateAndTime.GetCalendarTimeFromEpoch`, `C_DateAndTime.GetCurrentCalendarTime`, `C_DateAndTime.GetSecondsUntilDailyReset`, `C_DateAndTime.GetServerTimeLocal`, `C_Timer.After`, `C_Timer.NewTicker`, `C_Timer.NewTimer`, `GetServerTime`, `GetTickTime`, `GetTimeCached` |
|
||||
| [Totem](docs/API.md#totem) | `GetTotemInfo`, `GetTotemTimeLeft`, `GetTotemDuration`, `TargetTotem` |
|
||||
| [Tracking](docs/API.md#tracking) | `GetNumTrackingTypes`, `GetTrackingInfo`, `SetTracking` |
|
||||
@@ -161,6 +166,7 @@ the glue state because GlueXML had no way to reach them otherwise.
|
||||
| [CharacterList](docs/API.md#characterlist) | `GetSavedCharacterOrder`, `SetSavedCharacterOrder` (persist to `WTF\Account\...\ClassicAPI.txt`) |
|
||||
| CVar | `GetCVar`, `SetCVar`, `RegisterCVar`, `GetCVarDefault`, `C_CVar.GetCVarBool`, `C_CVar.SetTempCVar`, `C_CVar.RemoveTempCVar` (storage is process-global — writes from glue are visible in-world and vice versa) |
|
||||
| [Glue](docs/API.md#glue) | `C_Glue.IsFirstLoadThisSession`, `C_Glue.IsOnGlueScreen` |
|
||||
| [Input](docs/API.md#getcursordelta) | `GetCursorDelta` |
|
||||
| Script | `RunScript` (compile and run a Lua chunk in the glue state's globals — useful for slash-command-style helpers in GlueXML) |
|
||||
| State | `IsLoggedIn` |
|
||||
|
||||
@@ -219,12 +225,13 @@ open the game menu and click AddOn Options, then ClassicAPI.
|
||||
| [Bindings](docs/API.md#actionbuttonusekeydown) | `ActionButtonUseKeyDown` | `0` | `1` uses an action-bar key binding when you press the key. `0` uses it when you release the key. |
|
||||
| [Combat](docs/API.md#stopautoattackontargetchange) | `stopAutoAttackOnTargetChange` | `0` | `1` stops melee auto-attack, Auto Shot and wand shots when you change target. |
|
||||
| [Combat log](docs/API.md#kept-entries) | `combatLogRetentionTime` | `300` | The number of seconds that the combat log keeps an entry. |
|
||||
| [Cast settings](docs/API.md#cast-settings-autostand-autodismount-autounshift) | `autoStand` | `0` | `1` makes you stand up when you cast while you sit. |
|
||||
| | `autoDismount` | `0` | `1` makes you dismount when you cast while you are mounted. |
|
||||
| | `autoUnshift` | `0` | `1` makes you leave your form when a spell cannot be cast in it. |
|
||||
| [Stand, dismount and form](docs/API.md#stand-dismount-and-form-settings-autostand-autodismount-autounshift) | `autoStand` | `0` | `1` makes you stand up when you cast, loot or open the flight map while you sit. |
|
||||
| | `autoDismount` | `0` | `1` makes you dismount when you cast, use an object, loot, attack or open the flight map while you are mounted. |
|
||||
| | `autoUnshift` | `0` | `1` makes you leave your form when you cast a spell that cannot be cast in it, use an item, or open the flight map. It also lets you talk to NPCs and use objects in druid forms and Ghost Wolf. |
|
||||
| [Bag sorting](docs/API.md#c_containergetsortbagsrighttoleft--c_containersetsortbagsrighttoleftenable) | `sortBagsRightToLeft` | `0` | `1` makes a sort fill your bags from the last slot of the last bag. |
|
||||
| | `backpackAutosortDisabled` | `0` | `1` keeps the items in your backpack in place when you sort your bags. |
|
||||
| | `bankAutosortDisabled` | `0` | `1` keeps the items in the main bank slots in place when you sort your bank. |
|
||||
| [Expanded cursors](docs/API.md#expanded-cursors-expandedcursors) | `expandedCursors` | `0` | `1` makes the cursor show what you can do with an NPC or an object: an anvil for repairs, a hearthstone for an inn, a horseshoe for a stable, `!` or `?` for quests, and a fish hook for your fishing bobber. |
|
||||
| [Equipment sets](docs/API.md#equipment-sets-on-item-tooltips) | `dontShowEquipmentSetsOnItems` | `0` | `1` removes the Equipment Sets line from item tooltips. |
|
||||
| [Name completion](docs/API.md#name-completion-settings) | `autoCompleteResortNamesOnRecency` | `1` | `1` puts the players that you whispered first in `C_AutoComplete` results, the most recent first. `0` uses alphabetical order. |
|
||||
| | `autoCompleteWhenEditingFromCenter` | `1` | `1` also matches the text after the cursor when you type inside a name. |
|
||||
@@ -397,6 +404,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>
|
||||
@@ -504,10 +512,10 @@ the embedded addon.
|
||||
## Lua errors
|
||||
|
||||
ClassicAPI replaces the game's Lua error box with a Lua error window. The
|
||||
window opens when an error occurs. It shows the message, the time, the count
|
||||
and the stack of each error. You can select and copy the text. The arrow
|
||||
buttons show the previous and the next error. The **Reload UI** button
|
||||
reloads the interface.
|
||||
window opens when an error occurs. It shows the message, the time, the count,
|
||||
the stack and the local variables of each error. You can select and copy the
|
||||
text. The arrow buttons show the previous and the next error. The
|
||||
**Reload UI** button reloads the interface.
|
||||
|
||||
If you use `!BugGrabber` (with BugSack), BugGrabber gets the errors and the
|
||||
window does not open. If an addon sets its own error handler, the window also
|
||||
|
||||
+52
-12
@@ -1,5 +1,5 @@
|
||||
# embed_resources.cmake — embed the addons and the bindings XML as Win32
|
||||
# RCDATA resources.
|
||||
# embed_resources.cmake — embed the addons, the bindings XML and the
|
||||
# built-in textures as Win32 RCDATA resources.
|
||||
#
|
||||
# Runs at configure time. It writes two files into OUT_DIR:
|
||||
#
|
||||
@@ -16,20 +16,27 @@
|
||||
# constexpr File kFiles[] = { ... };
|
||||
# constexpr size_t kFileCount = ...;
|
||||
# }
|
||||
# namespace Texture::Builtin::Manifest {
|
||||
# struct File { const char *path; int resourceId; int index; };
|
||||
# constexpr File kFiles[] = { ... };
|
||||
# constexpr size_t kFileCount = ...;
|
||||
# }
|
||||
#
|
||||
# Each resource is a pack: one gzip stream of several files, which
|
||||
# pack_files.cmake joins at build time into `OUT_DIR/embed/<name>.pack.gz`.
|
||||
# Resource 1 is the bindings XML, a pack of one; then each addon is one
|
||||
# pack, in ADDONS order. A pack compresses each file against the files
|
||||
# before it, so the addon text packs to about 22% of its size, against 27%
|
||||
# for one gzip stream per file. The DLL inflates a whole pack with the
|
||||
# engine's zlib on first use (src/embed/Resource.cpp), so the
|
||||
# load-on-demand addon costs nothing until it loads.
|
||||
# pack, in ADDONS order; then the textures, one pack. A pack compresses each
|
||||
# file against the files before it, so the addon text packs to about 22% of
|
||||
# its size, against 27% for one gzip stream per file. The DLL inflates a
|
||||
# whole pack with the engine's zlib on first use (src/embed/Resource.cpp),
|
||||
# so the load-on-demand addon costs nothing until it loads.
|
||||
#
|
||||
# File paths are relative to ADDONS_ROOT, in backslash form
|
||||
# Addon file paths are relative to ADDONS_ROOT, in backslash form
|
||||
# (`!!!ClassicAPI\Util\Print.lua`) — the suffix the engine builds after
|
||||
# `Interface\AddOns\`. The DLL compares them ignoring case. A file's index
|
||||
# is its place in its addon's pack, which is in path order.
|
||||
# `Interface\AddOns\`. Texture paths are relative to TEXTURES_ROOT, where
|
||||
# each file sits at its game path (`Interface\Cursor\Quest.blp`), for
|
||||
# src/texture/Builtin.cpp. The DLL compares both ignoring case. A file's
|
||||
# index is its place in its pack, which is in path order.
|
||||
#
|
||||
# Each addon's `.toc` is the one file that is not embedded verbatim: its
|
||||
# `## Version:` line is stamped with ADDON_VERSION into a copy under
|
||||
@@ -46,7 +53,8 @@
|
||||
#
|
||||
# Usage:
|
||||
# embed_resources(TARGET <t> ADDONS_ROOT <dir> ADDONS <folder names...>
|
||||
# ADDON_VERSION <v> BINDINGS_XML <file> OUT_DIR <dir>)
|
||||
# ADDON_VERSION <v> BINDINGS_XML <file>
|
||||
# TEXTURES_ROOT <dir> OUT_DIR <dir>)
|
||||
|
||||
set(_EMBED_PACK_SCRIPT "${CMAKE_CURRENT_LIST_DIR}/pack_files.cmake")
|
||||
|
||||
@@ -81,7 +89,8 @@ function(_embed_write_if_changed path content)
|
||||
endfunction()
|
||||
|
||||
function(embed_resources)
|
||||
cmake_parse_arguments(E "" "TARGET;ADDONS_ROOT;ADDON_VERSION;BINDINGS_XML;OUT_DIR"
|
||||
cmake_parse_arguments(E ""
|
||||
"TARGET;ADDONS_ROOT;ADDON_VERSION;BINDINGS_XML;TEXTURES_ROOT;OUT_DIR"
|
||||
"ADDONS" ${ARGN})
|
||||
|
||||
set(rc "// Generated by cmake/embed_resources.cmake - do not edit.\n\n")
|
||||
@@ -125,6 +134,22 @@ function(embed_resources)
|
||||
math(EXPR id "${id} + 1")
|
||||
endforeach()
|
||||
|
||||
file(GLOB_RECURSE textures CONFIGURE_DEPENDS RELATIVE
|
||||
"${E_TEXTURES_ROOT}" "${E_TEXTURES_ROOT}/*")
|
||||
list(SORT textures) # deterministic indexes
|
||||
set(texture_paths "")
|
||||
set(texture_manifest "")
|
||||
set(index 0)
|
||||
foreach(rel ${textures})
|
||||
list(APPEND texture_paths "${E_TEXTURES_ROOT}/${rel}")
|
||||
string(REPLACE "/" "\\\\" rel_bs "${rel}")
|
||||
string(APPEND texture_manifest " { \"${rel_bs}\", ${id}, ${index} },\n")
|
||||
math(EXPR index "${index} + 1")
|
||||
endforeach()
|
||||
_embed_pack("${E_OUT_DIR}" "Textures" ${texture_paths})
|
||||
string(APPEND rc "${id} RCDATA \"${gz}\"\n")
|
||||
list(APPEND inputs "${gz}")
|
||||
|
||||
_embed_write_if_changed("${E_OUT_DIR}/embedded.rc" "${rc}")
|
||||
_embed_write_if_changed("${E_OUT_DIR}/embedded_resources.h"
|
||||
"// Generated by cmake/embed_resources.cmake — do not edit.
|
||||
@@ -159,6 +184,21 @@ ${manifest}};
|
||||
constexpr size_t kFileCount = sizeof(kFiles) / sizeof(kFiles[0]);
|
||||
|
||||
} // namespace Addons::Embedded::Manifest
|
||||
|
||||
namespace Texture::Builtin::Manifest {
|
||||
|
||||
struct File {
|
||||
const char *path; // the game path
|
||||
int resourceId; // the textures' pack
|
||||
int index; // its place in the pack
|
||||
};
|
||||
|
||||
constexpr File kFiles[] = {
|
||||
${texture_manifest}};
|
||||
|
||||
constexpr size_t kFileCount = sizeof(kFiles) / sizeof(kFiles[0]);
|
||||
|
||||
} // namespace Texture::Builtin::Manifest
|
||||
")
|
||||
|
||||
add_custom_target(${E_TARGET}_embed DEPENDS ${inputs})
|
||||
|
||||
+3367
-890
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -178,9 +178,9 @@ behaviour the comment describes (`push 0; mov edx, 1; mov ecx, L`) is
|
||||
| `0x006F4450` | `lua_next` | fastcall | calls `luaH_next` at `0x006FA2A0` | ★ |
|
||||
| `0x006F44E0` | `lua_concat` | fastcall | calls `luaV_concat` at `0x006F84D0` | ★ |
|
||||
| `0x006F4560` | `lua_newuserdata` | fastcall | tag 7; calls `luaS_newudata` at `0x006F9E30` | ★ |
|
||||
| `0x006F45B0` | `lua_getupvalue` | fastcall | reads upvalue from C closure | ◦ |
|
||||
| `0x006F4660` | `lua_setupvalue` | fastcall | | ◦ |
|
||||
| `0x006F46E0` | *internal upvalue helper*| | shared by get/set above | ◦ |
|
||||
| `0x006F45B0` | `lua_pushupvalues` | fastcall | pushes the running C closure's upvalues, returns how many | ★ |
|
||||
| `0x006F4660` | `lua_getupvalue` | fastcall | `(L, funcindex, n)`: pushes upvalue n, returns its name or null | ★ |
|
||||
| `0x006F46E0` | `aux_upvalue` | | finds upvalue n of a function, returns its name | ★ |
|
||||
|
||||
## Auxiliary library (`luaL_*`)
|
||||
|
||||
|
||||
@@ -64,6 +64,17 @@ bool RedirectJcc(uintptr_t site, uintptr_t expected, const void *target) {
|
||||
return PatchBytes(reinterpret_cast<uint8_t *>(site) + 2, &rel, sizeof rel);
|
||||
}
|
||||
|
||||
bool RedirectSlot(uintptr_t slot, uintptr_t expected, const void *target) {
|
||||
uintptr_t current = 0;
|
||||
std::memcpy(¤t, reinterpret_cast<const void *>(slot), sizeof current);
|
||||
const uintptr_t to = reinterpret_cast<uintptr_t>(target);
|
||||
if (current == to)
|
||||
return true;
|
||||
if (current != expected)
|
||||
return false;
|
||||
return PatchBytes(reinterpret_cast<void *>(slot), &to, sizeof to);
|
||||
}
|
||||
|
||||
bool ReplaceWithCall(uintptr_t site, const uint8_t *original, size_t len, const void *target) {
|
||||
uint8_t code[64];
|
||||
if (len < 5 || len > sizeof code)
|
||||
@@ -102,4 +113,20 @@ void EnsureParentDirs(const char *relPath) {
|
||||
}
|
||||
}
|
||||
|
||||
char FoldPathChar(char c) {
|
||||
if (c == '/')
|
||||
return '\\';
|
||||
if (c >= 'A' && c <= 'Z')
|
||||
return static_cast<char>(c + ('a' - 'A'));
|
||||
return c;
|
||||
}
|
||||
|
||||
bool SamePath(const char *a, const char *b) {
|
||||
for (; FoldPathChar(*a) == FoldPathChar(*b); ++a, ++b) {
|
||||
if (*a == '\0')
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace Common
|
||||
|
||||
@@ -28,6 +28,10 @@ bool RedirectCall(uintptr_t site, uintptr_t expected, const void *target);
|
||||
// at `site` to `target` when it still reaches `expected`. Same contract.
|
||||
bool RedirectJcc(uintptr_t site, uintptr_t expected, const void *target);
|
||||
|
||||
// RedirectCall for a function-pointer slot (a vtable entry): points the slot
|
||||
// at `slot` to `target` when it still holds `expected`. Same contract.
|
||||
bool RedirectSlot(uintptr_t slot, uintptr_t expected, const void *target);
|
||||
|
||||
// Writes `call target` plus NOP padding over the `len` bytes (5 to 64) of
|
||||
// code at `site` when they still hold `original`; true when the site
|
||||
// now calls `target`. A site holding anything else (another DLL's patch, a
|
||||
@@ -45,4 +49,11 @@ void BoundedCopy(char *dst, const char *src, size_t n);
|
||||
// directory. Directories that already exist are a no-op.
|
||||
void EnsureParentDirs(const char *relPath);
|
||||
|
||||
// `c` as game paths compare it: '/' as '\', and A-Z as a-z.
|
||||
char FoldPathChar(char c);
|
||||
|
||||
// Whether `a` and `b` are the same game path: equal after FoldPathChar, so
|
||||
// any case and either slash.
|
||||
bool SamePath(const char *a, const char *b);
|
||||
|
||||
} // namespace Common
|
||||
|
||||
+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) {
|
||||
|
||||
+49
-18
@@ -58,6 +58,18 @@ inline T &Ref(uintptr_t address) {
|
||||
return *reinterpret_cast<T *>(address);
|
||||
}
|
||||
|
||||
// An engine constant at a fixed VA, read each time it is used, so the value is
|
||||
// the one the engine's own code reads: a runtime patch shows, and so does a
|
||||
// global the engine's static initializers fill after this DLL loads. Converts
|
||||
// to T, so it stands in a float expression as a float would.
|
||||
//
|
||||
// const Game::Live<float> kSkin{Offsets::VAR_MOVER_SKIN};
|
||||
template <typename T>
|
||||
struct Live {
|
||||
uintptr_t address;
|
||||
operator T() const { return Read<T>(address); }
|
||||
};
|
||||
|
||||
// Calls `fn(object)` for each object of one of a frame's intrusive lists,
|
||||
// OFF_FRAME_REGION_LIST or OFF_FRAME_CHILD_LIST: node+4 next, node+8 object,
|
||||
// a NULL or low-bit-1 link ends it - the walk the engine's own frame code
|
||||
@@ -89,8 +101,10 @@ void *ResolveUnitToken(const char *token);
|
||||
|
||||
// UI pixels <-> internal layout (anchor) units. Script_SetPoint converts its
|
||||
// pixel offsets with `px * [VAR_COORD_DDC_WIDTH_SCALE] / ([VAR_COORD_ASPECT_COMPENSATION]
|
||||
// * UI_COORD_SCALE_UNIT)`; Script_GetLeft and Script_GetStringWidth push the
|
||||
// inverse. A region's LOCAL pixels must be multiplied by its effective scale
|
||||
// * [FLOAT_SCRIPT_COORD_SCALE])`; every script coordinate push (GetLeft,
|
||||
// GetStringWidth, GetFont's height, GetCursorPosition, OnCursorChanged) is the
|
||||
// inverse, `DivideByScreenScaleX(GetScaledScreenAspect() * 1024 * value)`,
|
||||
// which InternalToPixel calls in that order. A region's LOCAL pixels must be multiplied by its effective scale
|
||||
// (OFF_REGION_EFFECTIVE_SCALE) first. Both return 0 while the scale globals
|
||||
// are unset, so an early caller gets a zero offset rather than a wild one.
|
||||
float PixelToInternal(float px);
|
||||
@@ -147,6 +161,18 @@ int StrCmpI(const char *a, const char *b, int n = 0x7FFFFFFF);
|
||||
// literals + file-scope arrays): no heap, no dynamic init.
|
||||
// - Summaries are user-facing text: one sentence, plain words, present
|
||||
// tense, no patch versions.
|
||||
|
||||
// A view over a file-scope `const T[]` that takes its count from the array's
|
||||
// type, so no call site passes one; `{}` means none.
|
||||
template <typename T>
|
||||
struct ArrayRef {
|
||||
const T *data = nullptr;
|
||||
int count = 0;
|
||||
constexpr ArrayRef() = default;
|
||||
template <size_t N>
|
||||
constexpr ArrayRef(const T (&a)[N]) : data(a), count(static_cast<int>(N)) {}
|
||||
};
|
||||
|
||||
namespace Doc {
|
||||
|
||||
struct Field {
|
||||
@@ -172,14 +198,7 @@ constexpr Field Vararg(const char *type, const char *doc = nullptr) {
|
||||
return {"...", type, true, nullptr, doc};
|
||||
}
|
||||
|
||||
// A view over a file-scope `const Field[]`; `{}` means none.
|
||||
struct FieldList {
|
||||
const Field *data = nullptr;
|
||||
int count = 0;
|
||||
constexpr FieldList() = default;
|
||||
template <size_t N>
|
||||
constexpr FieldList(const Field (&a)[N]) : data(a), count(static_cast<int>(N)) {}
|
||||
};
|
||||
using FieldList = ArrayRef<Field>;
|
||||
|
||||
// A registered function. Positional init: `{summary, args, rets}` for a
|
||||
// table function, `{summary, args, rets, "XGlobals"}` for a global, and
|
||||
@@ -200,6 +219,7 @@ struct Method {
|
||||
const char *name;
|
||||
const Function *doc;
|
||||
};
|
||||
using MethodList = ArrayRef<Method>;
|
||||
|
||||
// A table shape a function returns or accepts → `Tables{Type="Structure"}`
|
||||
// in `system`. Self-registering: nothing binds a structure to Lua, so it
|
||||
@@ -240,7 +260,9 @@ constexpr int UpvalueIndex(int i) { return GLOBALS_INDEX - i; }
|
||||
// `lua_call` / `lua_pcall` nresults sentinel meaning "all".
|
||||
constexpr int MULTRET = -1;
|
||||
|
||||
// Type tag values returned by `Type()` (lua_type).
|
||||
// Type tag values returned by `Type()` (lua_type). TYPE_NONE is an absent
|
||||
// argument (past the top of the stack).
|
||||
constexpr int TYPE_NONE = -1;
|
||||
constexpr int TYPE_NIL = 0;
|
||||
constexpr int TYPE_BOOLEAN = 1;
|
||||
constexpr int TYPE_LIGHTUSERDATA = 2;
|
||||
@@ -479,6 +501,8 @@ void *ResolveTexture(void *L, int idx = 1, bool raiseError = true);
|
||||
void *ResolveFontString(void *L, int idx = 1, bool raiseError = true);
|
||||
void *ResolveEditBox(void *L, int idx = 1, bool raiseError = true);
|
||||
void *ResolveModel(void *L, int idx = 1, bool raiseError = true);
|
||||
void *ResolvePlayerModel(void *L, int idx = 1, bool raiseError = true);
|
||||
void *ResolveDressUpModel(void *L, int idx = 1, bool raiseError = true);
|
||||
|
||||
// Every name these registrars bind is ALSO bound under `_G.ClassicAPI` —
|
||||
// `ClassicAPI.GetSpellInfo`, `ClassicAPI.C_Item.IsBound` — as an escape
|
||||
@@ -523,10 +547,11 @@ struct FrameMethodEntry {
|
||||
|
||||
// Registers a batch of methods on a per-frame-type registry (e.g.
|
||||
// GameTooltipMethodRegistry for `tooltip:Foo()` calls). `context` is the
|
||||
// registry address — see Offsets::VAR_*_METHOD_REGISTRY. `docs` pairs
|
||||
// descriptors to the entries by NAME (any order, any subset).
|
||||
void RegisterFrameMethods(void *context, const FrameMethodEntry *table, int count,
|
||||
const Doc::Method *docs = nullptr, int docCount = 0);
|
||||
// registry address — see Offsets::VAR_*_METHOD_REGISTRY. `methods` and
|
||||
// `docs` are file-scope arrays; `docs` pairs descriptors to the entries by
|
||||
// NAME (any order, any subset).
|
||||
void RegisterFrameMethods(void *context, ArrayRef<FrameMethodEntry> methods,
|
||||
Doc::MethodList docs = {});
|
||||
|
||||
// Registers `func` at `_G[tableName][methodName]`, creating the namespace
|
||||
// table if it doesn't already exist. This is how modern WoW C_*-style APIs
|
||||
@@ -557,14 +582,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,
|
||||
// `entries` is a file-scope array.
|
||||
void RegisterIntegerEnum(const char *parent, const char *sub, ArrayRef<EnumIntegerEntry> entries,
|
||||
const char *docSystem = nullptr);
|
||||
|
||||
// Registers `Constants[sub] = { entries }`, Blizzard's engine constant
|
||||
// tables (`Constants.MoneyFormattingConstants`). Documented as a "Constants"
|
||||
// table of `docSystem`.
|
||||
void RegisterConstants(const char *sub, 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 +802,12 @@ bool RunHookRegistrations();
|
||||
// issue #33, Frame::Attributes). A pure-C++ cache keyed by spellID/itemID is
|
||||
// NOT fragile and must not register.
|
||||
//
|
||||
// It runs AFTER the frames are gone: CGGameUI::Shutdown destroys every frame
|
||||
// (and each Model frame's scene with every model in it) before
|
||||
// CGGameUI::Initialize resets the Lua state. So a cleanup here only FORGETS
|
||||
// pointers; it never releases an engine object tied to a frame. Release those
|
||||
// when the frame dies (Model::Teardown for Model frames).
|
||||
//
|
||||
// Same lifetime + ordering rules as ModuleAutoRegister: static-init chaining,
|
||||
// undefined cross-TU order — fine, because each callback only clears its OWN
|
||||
// state. This is the "clear stale state" category only; a save-before-teardown
|
||||
|
||||
+2311
-69
File diff suppressed because it is too large
Load Diff
+1
-3
@@ -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};
|
||||
|
||||
+11
-30
@@ -65,6 +65,7 @@
|
||||
|
||||
#include "embedded_resources.h"
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "addons/Embedded.h"
|
||||
@@ -76,6 +77,7 @@
|
||||
#include "addons/TocRewrite.h"
|
||||
#include "bindings/Inject.h"
|
||||
#include "embed/Resource.h"
|
||||
#include "storm/Memory.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
@@ -103,26 +105,12 @@ constexpr const char *kDevMarkerFile = ".classicapi-dev";
|
||||
// paths are case-insensitive.
|
||||
constexpr const char *kAddOnsPrefix = "Interface\\AddOns\\";
|
||||
|
||||
char NormalizeChar(char c) {
|
||||
if (c == '/') return '\\';
|
||||
if (c >= 'A' && c <= 'Z') return static_cast<char>(c + 32);
|
||||
return c;
|
||||
}
|
||||
|
||||
bool PathEqualsCI(const char *a, const char *b) {
|
||||
while (*a && *b) {
|
||||
if (NormalizeChar(*a) != NormalizeChar(*b)) return false;
|
||||
++a; ++b;
|
||||
}
|
||||
return *a == *b;
|
||||
}
|
||||
|
||||
// `path` with the case-insensitive `prefix` stripped, NULL when it does
|
||||
// not start with it.
|
||||
const char *StripPrefixCI(const char *path, const char *prefix) {
|
||||
while (*prefix) {
|
||||
if (*path == '\0') return nullptr;
|
||||
if (NormalizeChar(*path) != NormalizeChar(*prefix)) return nullptr;
|
||||
if (Common::FoldPathChar(*path) != Common::FoldPathChar(*prefix)) return nullptr;
|
||||
++path; ++prefix;
|
||||
}
|
||||
return path;
|
||||
@@ -130,7 +118,7 @@ const char *StripPrefixCI(const char *path, const char *prefix) {
|
||||
|
||||
int IndexOfAddon(const char *name) {
|
||||
for (size_t i = 0; i < kAddonCount; ++i) {
|
||||
if (PathEqualsCI(name, kAddons[i]))
|
||||
if (Common::SamePath(name, kAddons[i]))
|
||||
return static_cast<int>(i);
|
||||
}
|
||||
return -1;
|
||||
@@ -144,7 +132,7 @@ int MatchAddonPath(const char *path, const char **relPath) {
|
||||
if (rel == nullptr) return -1;
|
||||
for (size_t i = 0; i < kAddonCount; ++i) {
|
||||
const char *rest = StripPrefixCI(rel, kAddons[i]);
|
||||
if (rest != nullptr && NormalizeChar(*rest) == '\\') {
|
||||
if (rest != nullptr && Common::FoldPathChar(*rest) == '\\') {
|
||||
*relPath = rel;
|
||||
return static_cast<int>(i);
|
||||
}
|
||||
@@ -155,12 +143,10 @@ int MatchAddonPath(const char *path, const char **relPath) {
|
||||
// The embedded bytes for manifest path `relPath`; `data` is NULL when it
|
||||
// is not embedded.
|
||||
Embed::Bytes LookupEmbedded(const char *relPath) {
|
||||
for (size_t i = 0; i < Manifest::kFileCount; ++i) {
|
||||
if (PathEqualsCI(relPath, Manifest::kFiles[i].path))
|
||||
return Embed::File(Manifest::kFiles[i].resourceId,
|
||||
Manifest::kFiles[i].index);
|
||||
}
|
||||
return {nullptr, 0};
|
||||
const Manifest::File *file = Embed::FindFile(Manifest::kFiles, relPath);
|
||||
if (file == nullptr)
|
||||
return {nullptr, 0};
|
||||
return Embed::File(file->resourceId, file->index);
|
||||
}
|
||||
|
||||
// Engine file I/O + Storm allocator — see addons/EngineIO.h for the shapes and
|
||||
@@ -168,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).
|
||||
@@ -292,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.
|
||||
@@ -335,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
|
||||
|
||||
+1
-5
@@ -408,11 +408,7 @@ static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_AddOns", "GetAddOnInterfaceVersion",
|
||||
&Script_GetAddOnInterfaceVersion,
|
||||
&kGetAddOnInterfaceVersion);
|
||||
Game::Lua::RegisterIntegerEnum(
|
||||
"Enum", "AddOnSecurityStatus",
|
||||
kAddOnSecurityStatusEntries,
|
||||
sizeof(kAddOnSecurityStatusEntries) /
|
||||
sizeof(kAddOnSecurityStatusEntries[0]));
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "AddOnSecurityStatus", kAddOnSecurityStatusEntries);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#include "addons/EngineIO.h"
|
||||
#include "addons/Toc.h"
|
||||
#include "luasyntax/AddonNamespace.h"
|
||||
#include "storm/Memory.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -51,7 +52,6 @@ using IsLoadedFn_t = uint8_t(__fastcall *)(const char *name);
|
||||
// FUN_FILE_READ address routes through Embedded's file-read hook, which forwards
|
||||
// every non-`!!!ClassicAPI` path to the original untouched — what we want here.
|
||||
using AddOns::EngineIO::FileReadFn;
|
||||
using AddOns::EngineIO::SMemFreeFn;
|
||||
|
||||
// True iff the addon's `.toc` has a line `## AllowAddOnTableAccess: <nonzero>`.
|
||||
// The value is read as a TOC boolean flag: parse the leading integer, true iff
|
||||
@@ -87,8 +87,7 @@ bool TocAllowsTableAccess(const char *name) {
|
||||
allowed = anyDigit && value != 0;
|
||||
}
|
||||
|
||||
auto smemFree = reinterpret_cast<SMemFreeFn>(Offsets::FUN_STORM_SMEM_FREE);
|
||||
smemFree(buf, __FILE__, __LINE__, 0);
|
||||
Storm::Free(buf, __FILE__, __LINE__);
|
||||
return allowed;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -665,10 +665,15 @@ void RecordTable(const char *table, const char *name, const Function *doc) {
|
||||
Record(KIND_TABLE, table, name, nullptr, doc);
|
||||
}
|
||||
|
||||
void RecordFrameMethods(const void *registry, const Game::Lua::FrameMethodEntry *table,
|
||||
int count, const Method *docs, int docCount) {
|
||||
void RecordFrameMethods(const void *registry,
|
||||
Game::ArrayRef<Game::Lua::FrameMethodEntry> methods,
|
||||
Game::Doc::MethodList docList) {
|
||||
if (g_pass == ENV_NONE)
|
||||
return;
|
||||
const Game::Lua::FrameMethodEntry *table = methods.data;
|
||||
const int count = methods.count;
|
||||
const Method *docs = docList.data;
|
||||
const int docCount = docList.count;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const Function *doc = nullptr;
|
||||
for (int d = 0; d < docCount; ++d) {
|
||||
@@ -720,14 +725,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
|
||||
|
||||
+2
-9
@@ -1023,16 +1023,9 @@ CompressState *FindOrClaimCompressState(uint64_t targetGuid,
|
||||
// would compress it, this gate excludes it exactly as the server's does.
|
||||
// (Drain Soul passes via its effect-2 periodic damage: aura=[86,3,0].)
|
||||
bool HasPeriodicDamageOrLeech(const uint8_t *rec) {
|
||||
if (rec == nullptr)
|
||||
return false;
|
||||
constexpr int32_t kPeriodicDamage = 3, kPeriodicLeech = 53;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
const int32_t aura = *reinterpret_cast<const int32_t *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME + i * 4);
|
||||
if (aura == kPeriodicDamage || aura == kPeriodicLeech)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return Spell::Lookup::HasAuraEffect(rec, kPeriodicDamage) ||
|
||||
Spell::Lookup::HasAuraEffect(rec, kPeriodicLeech);
|
||||
}
|
||||
|
||||
// Compression pairs by CASTER, mirroring the server (each warlock's Dark
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `debuglocals([level [, skipFunctionsAndUserdata]])` — the locals and then
|
||||
// the upvalues of a call level as text, one `name=value` line each, for an
|
||||
// error report. 4.3.4's Script_debuglocals and the FrameScript_PackVariable
|
||||
// it formats with, in retail's spacing (probed: `n=7`, `s="hi"`,
|
||||
// `t=<table>{`, ` k=1`, `}`; 4.3.4 writes `n = 7` and `t = <table> {`):
|
||||
// - Only while an error handler runs; otherwise it returns nothing. 4.3.4
|
||||
// tests a flag its error function sets around the handler call. 1.12's
|
||||
// error function sets none, but while a handler runs that function's
|
||||
// level is on the stack (ErrorCallstack::ErrorFunctionLevel).
|
||||
// - `level` counts as lua_getstack and debugstack do: 1 is the caller.
|
||||
// Less than 1 is 1.
|
||||
// - A table prints as `name=<table>{`, or with the frame's name for a
|
||||
// frame (t[0], as FrameScript_GetObject reads it), then its fields one
|
||||
// space in at the top level only, then `}`. A function prints where it
|
||||
// is defined. The text stops at 4095 bytes, each line at 1023.
|
||||
// Retail's second argument, which its Blizzard_ScriptErrors passes as true,
|
||||
// leaves out the function and userdata values, at the top level and in
|
||||
// tables.
|
||||
//
|
||||
// A number key in int range prints truncated with %d, as 4.3.4's
|
||||
// IsIntegerNumber tests only the range: 2.5 prints as 2. Two departures from
|
||||
// 4.3.4, both where it would fail inside the handler: a number key outside
|
||||
// that range is formatted from a copy, not converted in place (that breaks
|
||||
// lua_next), and a key that is not a number or string prints as its type in
|
||||
// angle brackets (4.3.4 formats a null string).
|
||||
//
|
||||
// Retail also lists `(*temporary)` lines: 5.1's lua_getlocal names each stack
|
||||
// slot past a level's locals so, a C level's arguments included (an
|
||||
// `error("x")` report shows `(*temporary)="x"`). 1.12's lua_getlocal
|
||||
// (0x6FBB20) is 5.0's, which names declared locals only. Those slots are
|
||||
// the compiler's registers, which 5.0 and 5.1 allocate differently, so they
|
||||
// are left out rather than imitated.
|
||||
//
|
||||
// Registered on both Lua states, like the base-library helpers.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "baselib/ErrorCallstack.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace BaseLib::DebugLocals {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace Lua = Game::Lua;
|
||||
|
||||
using lua_getlocal_t = const char *(__fastcall *)(void *L, const void *ar, int n);
|
||||
using lua_getupvalue_t = const char *(__fastcall *)(void *L, int funcindex, int n);
|
||||
|
||||
const auto GetLocal = reinterpret_cast<lua_getlocal_t>(Offsets::LUA_GET_LOCAL);
|
||||
const auto GetUpvalue = reinterpret_cast<lua_getupvalue_t>(Offsets::LUA_GET_UPVALUE);
|
||||
|
||||
constexpr size_t kTextMax = 4095; // 4.3.4's 4096-byte buffer, less its NUL
|
||||
constexpr size_t kLineSize = 1024;
|
||||
|
||||
struct Packer {
|
||||
void *L;
|
||||
bool skipFunctionsAndUserdata;
|
||||
std::string text;
|
||||
|
||||
void Line(const char *fmt, const char *indent, const char *name, const char *value = "",
|
||||
int number = 0) {
|
||||
char line[kLineSize];
|
||||
std::snprintf(line, sizeof line, fmt, indent, name, value, number);
|
||||
const size_t room = kTextMax - text.size();
|
||||
text.append(line, std::min(room, std::strlen(line)));
|
||||
}
|
||||
|
||||
// The value on the stack top, as 4.3.4's FrameScript_PackVariable.
|
||||
void Pack(const char *name, int depth) {
|
||||
const char *indent = depth > 0 ? " " : "";
|
||||
switch (Lua::Type(L, -1)) {
|
||||
case Lua::TYPE_NIL:
|
||||
Line("%s%s=nil\n", indent, name);
|
||||
break;
|
||||
case Lua::TYPE_BOOLEAN:
|
||||
Line("%s%s=%s\n", indent, name, Lua::ToBoolean(L, -1) ? "true" : "false");
|
||||
break;
|
||||
case Lua::TYPE_LIGHTUSERDATA:
|
||||
case Lua::TYPE_USERDATA:
|
||||
if (!skipFunctionsAndUserdata)
|
||||
Line("%s%s=<userdata>\n", indent, name);
|
||||
break;
|
||||
case Lua::TYPE_NUMBER:
|
||||
Line("%s%s=%s\n", indent, name, Lua::ToString(L, -1)); // a copy: safe to convert
|
||||
break;
|
||||
case Lua::TYPE_STRING:
|
||||
Line("%s%s=\"%s\"\n", indent, name, Lua::ToString(L, -1));
|
||||
break;
|
||||
case Lua::TYPE_TABLE:
|
||||
PackTable(name, depth, indent);
|
||||
break;
|
||||
case Lua::TYPE_FUNCTION:
|
||||
if (!skipFunctionsAndUserdata)
|
||||
PackFunction(name, indent);
|
||||
break;
|
||||
default:
|
||||
Line("%s%s=<%s>\n", indent, name, Lua::TypeName(L, Lua::Type(L, -1)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void PackTable(const char *name, int depth, const char *indent) {
|
||||
void *object = Lua::ResolveObject(L, -1);
|
||||
Line("%s%s=%s{\n", indent, name, object ? Lua::ObjectDisplayName(object) : "<table>");
|
||||
if (depth == 0) {
|
||||
Lua::PushNil(L);
|
||||
while (Lua::Next(L, -2) != 0) {
|
||||
char key[32];
|
||||
const char *keyName = key;
|
||||
const int keyType = Lua::Type(L, -2);
|
||||
const double number = keyType == Lua::TYPE_NUMBER ? Lua::ToNumber(L, -2) : 0;
|
||||
if (keyType == Lua::TYPE_NUMBER && std::isfinite(number) && number >= -2147483648.0 &&
|
||||
number <= 2147483647.0) {
|
||||
std::snprintf(key, sizeof key, "%d", static_cast<int>(number));
|
||||
} else if (keyType == Lua::TYPE_STRING) {
|
||||
keyName = Lua::ToString(L, -2);
|
||||
} else if (keyType == Lua::TYPE_NUMBER) {
|
||||
Lua::PushValue(L, -2);
|
||||
std::snprintf(key, sizeof key, "%s", Lua::ToString(L, -1));
|
||||
Lua::SetTop(L, -2);
|
||||
} else {
|
||||
std::snprintf(key, sizeof key, "<%s>", Lua::TypeName(L, keyType));
|
||||
}
|
||||
Pack(keyName, 1);
|
||||
Lua::SetTop(L, -2); // the value; the key stays for lua_next
|
||||
}
|
||||
}
|
||||
Line("%s}\n", indent, "");
|
||||
}
|
||||
|
||||
void PackFunction(const char *name, const char *indent) {
|
||||
alignas(4) uint8_t ar[Offsets::SIZEOF_LUA_DEBUG];
|
||||
Lua::PushValue(L, -1);
|
||||
Lua::GetInfo(L, ">nS", ar); // pops the copy
|
||||
const auto field = [&ar](uintptr_t offset) { return Game::Read<const char *>(ar, offset); };
|
||||
const char *source = field(Offsets::OFF_LUA_DEBUG_SOURCE);
|
||||
const int line = Game::Read<int>(ar, Offsets::OFF_LUA_DEBUG_LINEDEFINED);
|
||||
if (const char *fn = field(Offsets::OFF_LUA_DEBUG_NAME)) {
|
||||
char named[kLineSize];
|
||||
std::snprintf(named, sizeof named, "%s() defined %s", fn, source ? source : "?");
|
||||
Line("%s%s=%s:%d\n", indent, name, named, line);
|
||||
} else {
|
||||
Line("%s%s=<function> defined %s:%d\n", indent, name, source ? source : "?", line);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
int __fastcall Script_debuglocals(void *L) {
|
||||
if (ErrorCallstack::ErrorFunctionLevel(L) == 0)
|
||||
return 0;
|
||||
int level = static_cast<int>(Lua::ToNumber(L, 1));
|
||||
if (level <= 0)
|
||||
level = 1;
|
||||
Packer packer{L, Lua::ToBoolean(L, 2) != 0, {}};
|
||||
|
||||
alignas(4) uint8_t ar[Offsets::SIZEOF_LUA_DEBUG];
|
||||
if (Lua::GetStack(L, level, ar)) {
|
||||
const char *name;
|
||||
for (int i = 1; (name = GetLocal(L, ar, i)) != nullptr; i++) {
|
||||
packer.Pack(name, 0);
|
||||
Lua::SetTop(L, -2);
|
||||
}
|
||||
Lua::GetInfo(L, "f", ar); // the level's function
|
||||
for (int i = 1; (name = GetUpvalue(L, -1, i)) != nullptr; i++) {
|
||||
packer.Pack(name, 0);
|
||||
Lua::SetTop(L, -2);
|
||||
}
|
||||
Lua::SetTop(L, -2);
|
||||
}
|
||||
Lua::PushLString(L, packer.text.data(), static_cast<unsigned int>(packer.text.size()));
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kDebuglocalsArgs[] = {
|
||||
Game::Doc::Opt("level", "number", "1", "The call level, as debugstack counts it."),
|
||||
Game::Doc::Opt("skipFunctionsAndUserdata", "boolean", "false",
|
||||
"Leaves out function and userdata values."),
|
||||
};
|
||||
const Game::Doc::Field kDebuglocalsRets[] = {
|
||||
Game::Doc::Opt("locals", "string", nullptr,
|
||||
"One `name=value` line for each local and upvalue. Nothing outside an "
|
||||
"error handler."),
|
||||
};
|
||||
const Game::Doc::Function kDebuglocals{
|
||||
"The local variables and upvalues of a call level as text, for an error report.",
|
||||
kDebuglocalsArgs, kDebuglocalsRets, "LuaGlobals"};
|
||||
|
||||
void RegisterInGame() {
|
||||
Game::Lua::RegisterGlobalFunction("debuglocals", &Script_debuglocals, &kDebuglocals);
|
||||
}
|
||||
|
||||
void RegisterGlue() {
|
||||
Game::Lua::RegisterGlueFunction("debuglocals", &Script_debuglocals, &kDebuglocals);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterInGame};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterGlue};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::DebugLocals
|
||||
@@ -0,0 +1,102 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `debugstack([thread,] [start [, count1 [, count2]]])` — the stack of a
|
||||
// coroutine, as 4.3.4's Script_debugstack takes it: with a thread first,
|
||||
// the levels are that thread's and `start` defaults to 0 (the thread's
|
||||
// innermost level) instead of 1.
|
||||
//
|
||||
// 1.12's debugstack (FUN_SCRIPT_DEBUGSTACK) has no thread argument, and a
|
||||
// thread falls through its argument checks so that it returns the thread
|
||||
// itself: `msg .. debugstack(co)` raises "attempt to concatenate a thread
|
||||
// value" in the coroutine error paths of WeakAuras, Questie's ThreadLib,
|
||||
// MRT and Zygor.
|
||||
//
|
||||
// The engine's function still does the work, so its text and its head and
|
||||
// tail windows stay exactly as they are. It reads levels through six calls
|
||||
// (five lua_getstack, one lua_getinfo), and those calls are redirected to
|
||||
// read from the state Script_debugstack names for the call: the thread, or
|
||||
// the calling state when there is none. Script_debugstack replaces the
|
||||
// global and is the engine function's only way in, so the state is set for
|
||||
// every call; an error part way cannot leave a thread behind for the next.
|
||||
//
|
||||
// Registered on both Lua states, like the base-library helpers.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <initializer_list>
|
||||
|
||||
namespace BaseLib::DebugStack {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace Lua = Game::Lua;
|
||||
|
||||
// The state whose levels the engine's debugstack reads, or null for its own.
|
||||
void *g_levelsOf = nullptr;
|
||||
|
||||
int __fastcall GetStack(void *L, int level, void *ar) {
|
||||
return Lua::GetStack(g_levelsOf ? g_levelsOf : L, level, ar);
|
||||
}
|
||||
|
||||
int __fastcall GetInfo(void *L, const char *what, void *ar) {
|
||||
return Lua::GetInfo(g_levelsOf ? g_levelsOf : L, what, ar);
|
||||
}
|
||||
|
||||
int __fastcall Script_debugstack(void *L) {
|
||||
const auto engineDebugStack =
|
||||
reinterpret_cast<Lua::CFunction>(Offsets::FUN_SCRIPT_DEBUGSTACK);
|
||||
g_levelsOf = nullptr;
|
||||
if (Lua::Type(L, 1) == Lua::TYPE_THREAD) {
|
||||
g_levelsOf = Lua::ToThread(L, 1);
|
||||
Lua::Remove(L, 1);
|
||||
if (!Lua::IsNumber(L, 1)) { // start defaults to 0 for a thread
|
||||
Lua::PushNumber(L, 0);
|
||||
Lua::Insert(L, 1);
|
||||
}
|
||||
}
|
||||
const int results = engineDebugStack(L);
|
||||
g_levelsOf = nullptr;
|
||||
return results;
|
||||
}
|
||||
|
||||
void Apply() {
|
||||
const auto getStack = reinterpret_cast<const void *>(&GetStack);
|
||||
for (const uintptr_t site :
|
||||
{Offsets::CALL_DEBUGSTACK_GETSTACK_FIRST, Offsets::CALL_DEBUGSTACK_GETSTACK_TAIL_PROBE,
|
||||
Offsets::CALL_DEBUGSTACK_GETSTACK_TAIL_START, Offsets::CALL_DEBUGSTACK_GETSTACK_TAIL_SKIP,
|
||||
Offsets::CALL_DEBUGSTACK_GETSTACK_NEXT})
|
||||
Common::RedirectCall(site, Offsets::LUA_GET_STACK, getStack);
|
||||
Common::RedirectCall(Offsets::CALL_DEBUGSTACK_GETINFO, Offsets::LUA_GET_INFO,
|
||||
reinterpret_cast<const void *>(&GetInfo));
|
||||
}
|
||||
|
||||
void RegisterInGame() {
|
||||
Apply();
|
||||
Lua::RegisterGlobalFunction("debugstack", &Script_debugstack);
|
||||
}
|
||||
|
||||
void RegisterGlue() {
|
||||
Apply();
|
||||
Lua::RegisterGlueFunction("debugstack", &Script_debugstack);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterInGame};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterGlue};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::DebugStack
|
||||
@@ -40,12 +40,26 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "baselib/ErrorCallstack.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
namespace BaseLib::ErrorCallstack {
|
||||
|
||||
int ErrorFunctionLevel(void *L) {
|
||||
const auto errorFunction = reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_FRAMESCRIPT_CALL_ERROR_HANDLER);
|
||||
alignas(4) uint8_t ar[Offsets::SIZEOF_LUA_DEBUG];
|
||||
for (int level = 1; Game::Lua::GetStack(L, level, ar); ++level) {
|
||||
Game::Lua::GetInfo(L, "f", ar); // the level's function; nil for a lost tail call
|
||||
const bool found = Game::Lua::ToCFunction(L, -1) == errorFunction;
|
||||
Game::Lua::SetTop(L, -2);
|
||||
if (found)
|
||||
return level;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
std::optional<int> g_setHeight;
|
||||
@@ -59,21 +73,6 @@ int Height(void *L) {
|
||||
return height;
|
||||
}
|
||||
|
||||
// The innermost level whose function is the engine's error function, or 0
|
||||
// when no error is being reported.
|
||||
int ErrorFunctionLevel(void *L) {
|
||||
const auto errorFunction = reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_FRAMESCRIPT_CALL_ERROR_HANDLER);
|
||||
alignas(4) uint8_t ar[Offsets::SIZEOF_LUA_DEBUG];
|
||||
for (int level = 1; Game::Lua::GetStack(L, level, ar); ++level) {
|
||||
Game::Lua::GetInfo(L, "f", ar); // the level's function; nil for a lost tail call
|
||||
const bool found = Game::Lua::ToCFunction(L, -1) == errorFunction;
|
||||
Game::Lua::SetTop(L, -2);
|
||||
if (found)
|
||||
return level;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetCallstackHeight(void *L) {
|
||||
Game::Lua::PushNumber(L, Height(L));
|
||||
return 1;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace BaseLib::ErrorCallstack {
|
||||
|
||||
// The innermost call level whose function is the engine's error function
|
||||
// (FUN_FRAMESCRIPT_CALL_ERROR_HANDLER), or 0 when no error is being
|
||||
// reported. Nonzero exactly while an error handler runs.
|
||||
int ErrorFunctionLevel(void *L);
|
||||
|
||||
} // namespace BaseLib::ErrorCallstack
|
||||
+5
-21
@@ -47,7 +47,9 @@
|
||||
// section's header follows.
|
||||
|
||||
#include "Inject.h"
|
||||
#include "Common.h"
|
||||
#include "Offsets.h"
|
||||
#include "storm/Memory.h"
|
||||
|
||||
#include "embed/Resource.h"
|
||||
#include "embedded_resources.h"
|
||||
@@ -212,28 +214,12 @@ const char *FindSplicePosition(const char *content, size_t size,
|
||||
return FindOutsideComment(content, end, "</Bindings>");
|
||||
}
|
||||
|
||||
char NormalizeChar(char c) {
|
||||
if (c == '/') return '\\';
|
||||
if (c >= 'A' && c <= 'Z') return static_cast<char>(c + 32);
|
||||
return c;
|
||||
}
|
||||
|
||||
bool PathEqualsCI(const char *a, const char *b) {
|
||||
while (*a != '\0' && *b != '\0') {
|
||||
if (NormalizeChar(*a) != NormalizeChar(*b)) return false;
|
||||
++a; ++b;
|
||||
}
|
||||
return *a == *b;
|
||||
}
|
||||
|
||||
using SMemAlloc_t = void *(__stdcall *)(size_t size, const char *file, int line, int flags);
|
||||
using SMemFree_t = void(__stdcall *)(void *buf, const char *file, int line, int flags);
|
||||
|
||||
} // namespace
|
||||
|
||||
bool TryHandle(int unused, const char *path, void **outBuf, size_t *outSize,
|
||||
size_t extraBytes, int flag1, int flag2, FileReadFn original) {
|
||||
if (path == nullptr || !PathEqualsCI(path, kFrameXMLBindingsPath))
|
||||
if (path == nullptr || !Common::SamePath(path, kFrameXMLBindingsPath))
|
||||
return false;
|
||||
|
||||
// Pull the real file through the original reader with the caller's
|
||||
@@ -270,10 +256,8 @@ bool TryHandle(int unused, const char *path, void **outBuf, size_t *outSize,
|
||||
const size_t splicePos = static_cast<size_t>(splice - content);
|
||||
const size_t newSize = origSize + config.innerSize;
|
||||
|
||||
auto SMemAlloc = reinterpret_cast<SMemAlloc_t>(Offsets::FUN_STORM_SMEM_ALLOC);
|
||||
auto SMemFree = reinterpret_cast<SMemFree_t>(Offsets::FUN_STORM_SMEM_FREE);
|
||||
|
||||
void *newBuf = SMemAlloc(newSize + extraBytes, __FILE__, __LINE__, 0);
|
||||
void *newBuf = Storm::Alloc(newSize + extraBytes, __FILE__, __LINE__);
|
||||
if (newBuf == nullptr) {
|
||||
// Allocation failed — fall back to the original buffer so the
|
||||
// engine still loads its own bindings.
|
||||
@@ -290,7 +274,7 @@ bool TryHandle(int unused, const char *path, void **outBuf, size_t *outSize,
|
||||
if (extraBytes > 0)
|
||||
std::memset(dest + newSize, 0, extraBytes);
|
||||
|
||||
SMemFree(origBuf, __FILE__, __LINE__, 0);
|
||||
Storm::Free(origBuf, __FILE__, __LINE__);
|
||||
|
||||
if (outBuf != nullptr) *outBuf = newBuf;
|
||||
if (outSize != nullptr) *outSize = newSize;
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
#include "baselib/Ascii.h"
|
||||
#include "frame/ClickEvents.h"
|
||||
#include "input/Modifier.h"
|
||||
#include "storm/Memory.h"
|
||||
#include "xml/FrameLoad.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -219,8 +220,7 @@ void LoadModifiedClicks(const char *path, void *status) {
|
||||
return;
|
||||
void *tree = reinterpret_cast<TreeLoad_t>(Offsets::FUN_XML_TREE_LOAD)(
|
||||
static_cast<const char *>(data), static_cast<uint32_t>(size));
|
||||
reinterpret_cast<AddOns::EngineIO::SMemFreeFn>(Offsets::FUN_STORM_SMEM_FREE)(data, __FILE__,
|
||||
__LINE__, 0);
|
||||
Storm::Free(data, __FILE__, __LINE__);
|
||||
if (tree == nullptr)
|
||||
return; // the engine loader reports the parse error
|
||||
const void *root = Game::Read<const void *>(tree, 0);
|
||||
|
||||
+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};
|
||||
|
||||
+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;
|
||||
|
||||
@@ -145,12 +145,8 @@ void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("ConsoleGetAllCommands", &Script_GetAllCommands);
|
||||
Game::Lua::RegisterGlobalFunction("ConsolePrintAllMatchingCommands",
|
||||
&Script_ConsolePrintAllMatchingCommands);
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "ConsoleCommandType", kCommandTypeEntries,
|
||||
sizeof(kCommandTypeEntries) /
|
||||
sizeof(kCommandTypeEntries[0]));
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "ConsoleCategory", kCategoryEntries,
|
||||
sizeof(kCategoryEntries) /
|
||||
sizeof(kCategoryEntries[0]));
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "ConsoleCommandType", kCommandTypeEntries);
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "ConsoleCategory", kCategoryEntries);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -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};
|
||||
|
||||
+31
-13
@@ -55,6 +55,7 @@
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
#include "item/TypeInfo.h"
|
||||
#include "localization/Locale.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -86,12 +87,29 @@ char16_t ToLower(char16_t c) {
|
||||
return c;
|
||||
}
|
||||
|
||||
// Appends the lowercased `c` with 4.3.4's accent folding for `locale`
|
||||
// (frFR 2, deDE 3, esES/esMX 6/7, ruRU 8); æ, œ and ß become two letters.
|
||||
// Returns the new length.
|
||||
size_t AppendFolded(int locale, char16_t c, char16_t *out, size_t n) {
|
||||
switch (locale) {
|
||||
case 2: // frFR
|
||||
// 4.3.4's accent-folding sets. 4.3.4 picks one by its locale index (frFR 2,
|
||||
// deDE 3, esES/esMX 6/7, ruRU 8); 1.12's index has other slots, so the set
|
||||
// is picked by locale name.
|
||||
enum class Folding { None, French, German, Spanish, Russian };
|
||||
|
||||
Folding ClientFolding() {
|
||||
using Localization::Locale::Is;
|
||||
if (Is("frFR"))
|
||||
return Folding::French;
|
||||
if (Is("deDE"))
|
||||
return Folding::German;
|
||||
if (Is("esES") || Is("esMX"))
|
||||
return Folding::Spanish;
|
||||
if (Is("ruRU"))
|
||||
return Folding::Russian;
|
||||
return Folding::None;
|
||||
}
|
||||
|
||||
// Appends the lowercased `c` with `folding`'s accent folding; æ, œ and ß
|
||||
// become two letters. Returns the new length.
|
||||
size_t AppendFolded(Folding folding, char16_t c, char16_t *out, size_t n) {
|
||||
switch (folding) {
|
||||
case Folding::French:
|
||||
switch (c) {
|
||||
case 0xE0: case 0xE2: case 0xE4: c = u'a'; break;
|
||||
case 0xE6: out[n++] = u'a'; c = u'e'; break;
|
||||
@@ -103,14 +121,13 @@ size_t AppendFolded(int locale, char16_t c, char16_t *out, size_t n) {
|
||||
case 0xF9: case 0xFA: case 0xFB: case 0xFC: c = u'u'; break;
|
||||
}
|
||||
break;
|
||||
case 3: // deDE
|
||||
case Folding::German:
|
||||
if (c == 0xDF) {
|
||||
out[n++] = u's';
|
||||
c = u's';
|
||||
}
|
||||
break;
|
||||
case 6: // esES
|
||||
case 7: // esMX
|
||||
case Folding::Spanish:
|
||||
switch (c) {
|
||||
case 0xE1: c = u'a'; break;
|
||||
case 0xE9: c = u'e'; break;
|
||||
@@ -120,10 +137,12 @@ size_t AppendFolded(int locale, char16_t c, char16_t *out, size_t n) {
|
||||
case 0xF1: c = u'n'; break;
|
||||
}
|
||||
break;
|
||||
case 8: // ruRU
|
||||
case Folding::Russian:
|
||||
if (c == 0x451)
|
||||
c = 0x435;
|
||||
break;
|
||||
case Folding::None:
|
||||
break;
|
||||
}
|
||||
out[n++] = c;
|
||||
return n;
|
||||
@@ -141,11 +160,10 @@ size_t Fold(const char *text, char16_t (&out)[kFoldCap]) {
|
||||
// One unit short of the buffer: a full conversion stores no terminator.
|
||||
reinterpret_cast<Convert_t>(Offsets::FUN_SUNI_CONVERT_UTF8_TO_16)(
|
||||
wide, kFoldCap - 1, text, 0x7FFFFFFF, nullptr, nullptr);
|
||||
const int locale =
|
||||
*reinterpret_cast<const int *>(static_cast<uintptr_t>(Offsets::VAR_LOCALE_INDEX));
|
||||
const Folding folding = ClientFolding();
|
||||
size_t n = 0;
|
||||
for (size_t i = 0; wide[i] != 0 && n + 2 < kFoldCap; ++i)
|
||||
n = AppendFolded(locale, ToLower(wide[i]), out, n);
|
||||
n = AppendFolded(folding, ToLower(wide[i]), out, n);
|
||||
out[n] = 0;
|
||||
return n;
|
||||
}
|
||||
|
||||
@@ -131,10 +131,7 @@ int __fastcall Script_RequestLoadCreatureByID(void *L) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_CREATURE_CACHE);
|
||||
const bool accepted =
|
||||
Cache::QueryLoad::RequestLoad(cache, static_cast<uint32_t>(creatureID));
|
||||
Game::Lua::PushBool(L, accepted);
|
||||
Game::Lua::PushBool(L, RequestLoad(static_cast<uint32_t>(creatureID)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -381,4 +378,9 @@ const uint8_t *CacheRecord(uint32_t creatureID) {
|
||||
return creatureID != 0 ? PeekCreature(creatureID) : nullptr;
|
||||
}
|
||||
|
||||
bool RequestLoad(uint32_t creatureID) {
|
||||
return Cache::QueryLoad::RequestLoad(reinterpret_cast<void *>(Offsets::VAR_CREATURE_CACHE),
|
||||
creatureID);
|
||||
}
|
||||
|
||||
} // namespace Creature::Info
|
||||
|
||||
@@ -24,6 +24,12 @@ namespace Creature::Info {
|
||||
// engine's SetDisplay takes, and `PlayerModel:SetCreature` passes it there.
|
||||
const uint8_t *CacheRecord(uint32_t creatureID);
|
||||
|
||||
// Starts loading `creatureID` into the creature cache; fires
|
||||
// `CREATURE_DATA_LOAD_RESULT(creatureID, success)` when it lands (at once
|
||||
// when already cached). False only when the load could not be tracked.
|
||||
// Backs `C_CreatureInfo.RequestLoadCreatureByID`.
|
||||
bool RequestLoad(uint32_t creatureID);
|
||||
|
||||
// The faction group a player race belongs to, resolved the way
|
||||
// `Script_UnitFactionGroup` does it for a player whose object isn't in the
|
||||
// world (its name-cache branch, which reads the race): ChrRaces -> FactionTemplate
|
||||
|
||||
@@ -27,10 +27,8 @@ const Game::Lua::EnumIntegerEntry kMoneyFormattingConstants[] = {
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterConstants("MoneyFormattingConstants", kMoneyFormattingConstants,
|
||||
sizeof(kMoneyFormattingConstants) /
|
||||
sizeof(kMoneyFormattingConstants[0]),
|
||||
"MoneyConstants");
|
||||
Game::Lua::RegisterConstants(
|
||||
"MoneyFormattingConstants", kMoneyFormattingConstants, "MoneyConstants");
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -119,9 +119,7 @@ void OnTick() {
|
||||
const Tick::WorldTick::AutoSubscribe _tick{&OnTick};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterIntegerEnum(
|
||||
"Enum", "UICursorType", kCursorTypeEntries,
|
||||
sizeof(kCursorTypeEntries) / sizeof(kCursorTypeEntries[0]));
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "UICursorType", kCursorTypeEntries);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// The cursor modes later clients added, shown on 1.12's cursor, and the
|
||||
// `expandedCursors` cvar that turns them on (0 by default: they change what
|
||||
// 1.12 shows).
|
||||
//
|
||||
// The engine's cursor is a table of 42 images of 32x32, one per mode;
|
||||
// SendCursorImage copies slot VAR_CURSOR_MODE to the device. SetCursorMode
|
||||
// stores the mode it is given and sends, so a mode past 41 must never reach
|
||||
// it. Slot 41 is how the engine shows an image outside the table:
|
||||
// SetCustomCursor loads a named BLP into it, stores mode 41, sends it and
|
||||
// keeps the name, and returns at once while that name is already showing. The
|
||||
// game object classifier uses it for a record-named cursor.
|
||||
//
|
||||
// Show() calls SetCustomCursor with the mode's path, so a file of that name (a
|
||||
// patch MPQ, a loose file) wins, and a repeat each frame costs one name
|
||||
// compare. 1.12 has none of these files, so on a miss Show() does what
|
||||
// SetCustomCursor does after its load, with the image embedded in the DLL
|
||||
// (Texture::Builtin), which a texture load of the same path also gets.
|
||||
//
|
||||
// The files are 4.3.4's Interface\Cursor images, under 4.3.4's names. 4.3.4
|
||||
// already ships StableMaster.blp and Fishing.blp, but only as art a record
|
||||
// could name. StableMaster is a mode in retail's table (Enum.Cursormode
|
||||
// StablemasterCursor, listed after Ping and Enchant), not in 4.3.4's; no
|
||||
// client's table has Fishing.
|
||||
|
||||
#include "cursor/Extended.h"
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "cvar/Factory.h"
|
||||
#include "texture/Builtin.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace Cursor::Extended {
|
||||
|
||||
namespace {
|
||||
|
||||
using SetCustomCursor_t = uint32_t(__fastcall *)(const char *path);
|
||||
using SendCursorImage_t = void(__cdecl *)();
|
||||
|
||||
// In mode order, the Unable forms after the others.
|
||||
const char *const kPaths[] = {
|
||||
"Interface\\Cursor\\RepairNPC.blp",
|
||||
"Interface\\Cursor\\Innkeeper.blp",
|
||||
"Interface\\Cursor\\Quest.blp",
|
||||
"Interface\\Cursor\\QuestRepeatable.blp",
|
||||
"Interface\\Cursor\\QuestTurnIn.blp",
|
||||
"Interface\\Cursor\\StableMaster.blp",
|
||||
"Interface\\Cursor\\Fishing.blp",
|
||||
"Interface\\Cursor\\UnableRepairNPC.blp",
|
||||
"Interface\\Cursor\\UnableInnkeeper.blp",
|
||||
"Interface\\Cursor\\UnableQuest.blp",
|
||||
"Interface\\Cursor\\UnableQuestRepeatable.blp",
|
||||
"Interface\\Cursor\\UnableQuestTurnIn.blp",
|
||||
"Interface\\Cursor\\UnableStableMaster.blp",
|
||||
"Interface\\Cursor\\UnableFishing.blp",
|
||||
};
|
||||
static_assert(sizeof kPaths / sizeof kPaths[0] == 2 * kUnableOffset, "one path per mode");
|
||||
|
||||
CVar::Factory::Handle g_enabled = nullptr;
|
||||
|
||||
void Register() {
|
||||
g_enabled = CVar::Factory::Register(
|
||||
"expandedCursors",
|
||||
"Show what an NPC or object offers on the cursor: anvil for repairs, ! and ? for quests, "
|
||||
"hook for your bobber",
|
||||
Offsets::CVAR_CATEGORY_GAME, "0");
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Register};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool Enabled() { return CVar::Factory::GetInt(g_enabled) != 0; }
|
||||
|
||||
bool Is(uint32_t mode) { return mode >= kRepairNpc && mode < kRepairNpc + 2 * kUnableOffset; }
|
||||
|
||||
bool IsUnable(uint32_t mode) { return Is(mode) && mode >= kRepairNpc + kUnableOffset; }
|
||||
|
||||
bool IsQuest(uint32_t mode) {
|
||||
if (!Is(mode))
|
||||
return false;
|
||||
const uint32_t kind = IsUnable(mode) ? mode - kUnableOffset : mode;
|
||||
return kind >= kQuest && kind <= kQuestTurnIn;
|
||||
}
|
||||
|
||||
const char *Path(uint32_t mode) { return kPaths[mode - kRepairNpc]; }
|
||||
|
||||
void Show(uint32_t mode) {
|
||||
const char *path = Path(mode);
|
||||
if (reinterpret_cast<SetCustomCursor_t>(Offsets::FUN_SET_CUSTOM_CURSOR)(path) != 0)
|
||||
return;
|
||||
const Texture::Builtin::Image *image = Texture::Builtin::Find(path);
|
||||
if (image == nullptr || image->width != Offsets::CURSOR_IMAGE_SIZE ||
|
||||
image->height != Offsets::CURSOR_IMAGE_SIZE)
|
||||
return;
|
||||
Game::Ref<uint32_t>(Offsets::VAR_CURSOR_MODE) = Offsets::CURSOR_MODE_CUSTOM;
|
||||
std::memcpy(reinterpret_cast<void *>(Offsets::VAR_CURSOR_IMAGE_CUSTOM), image->texels,
|
||||
Offsets::CURSOR_IMAGE_SIZE * Offsets::CURSOR_IMAGE_SIZE * sizeof(uint32_t));
|
||||
reinterpret_cast<SendCursorImage_t>(Offsets::FUN_SEND_CURSOR_IMAGE)();
|
||||
Common::BoundedCopy(reinterpret_cast<char *>(Offsets::VAR_CUSTOM_CURSOR_NAME), path,
|
||||
Offsets::CURSOR_FILE_SIZE);
|
||||
}
|
||||
|
||||
} // namespace Cursor::Extended
|
||||
@@ -0,0 +1,57 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Cursor::Extended {
|
||||
|
||||
// The cursor modes later clients added that 1.12 has no image for, numbered
|
||||
// on from the engine's last mode (CURSOR_MODE_CUSTOM): 4.3.4's RepairNPC,
|
||||
// Innkeeper and quest cursors, retail's StableMaster, and the fish hook for
|
||||
// the fishing bobber. As with the engine's modes, each has an Unable form,
|
||||
// kUnableOffset further on.
|
||||
constexpr uint32_t kRepairNpc = Offsets::CURSOR_MODE_CUSTOM + 1;
|
||||
constexpr uint32_t kInnkeeper = kRepairNpc + 1;
|
||||
constexpr uint32_t kQuest = kRepairNpc + 2;
|
||||
constexpr uint32_t kQuestRepeatable = kRepairNpc + 3;
|
||||
constexpr uint32_t kQuestTurnIn = kRepairNpc + 4;
|
||||
constexpr uint32_t kStableMaster = kRepairNpc + 5;
|
||||
constexpr uint32_t kFishing = kRepairNpc + 6;
|
||||
constexpr uint32_t kUnableOffset = 7;
|
||||
|
||||
// Whether the `expandedCursors` cvar asks for these modes. The pickers that
|
||||
// give them (cursor/Npc, cursor/Fishing) keep 1.12's cursor while it is 0.
|
||||
bool Enabled();
|
||||
|
||||
// Whether `mode` is one of these, in either form.
|
||||
bool Is(uint32_t mode);
|
||||
|
||||
// Whether `mode` is one of these in its Unable form.
|
||||
bool IsUnable(uint32_t mode);
|
||||
|
||||
// Whether `mode` is a quest giver's cursor (Quest, QuestRepeatable,
|
||||
// QuestTurnIn), in either form.
|
||||
bool IsQuest(uint32_t mode);
|
||||
|
||||
// The image file of one of these modes, `Interface\Cursor\<its name>.blp`.
|
||||
const char *Path(uint32_t mode);
|
||||
|
||||
// Shows one of these modes as the cursor. The engine's SetCursorMode cannot,
|
||||
// so its hook (cursor/Hover) sends them here.
|
||||
void Show(uint32_t mode);
|
||||
|
||||
} // namespace Cursor::Extended
|
||||
@@ -0,0 +1,67 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// With `expandedCursors` at 1 (Cursor::Extended::Enabled), the cursor over
|
||||
// the player's fishing bobber is a fish hook in place of the Interact gear.
|
||||
//
|
||||
// A game object's cursor is data. GetCursor (FUN_GAMEOBJECT_GET_CURSOR) gives
|
||||
// the cursor its record names, else its type handler's: BaseHandlerGetCursor
|
||||
// for most types, which gives a lock's LockType cursor, else Interact. The
|
||||
// record's name comes from the server, and tortoise-wow sends it empty; even
|
||||
// 3.3.5's data names none for the bobber (AzerothCore's template 35591), and
|
||||
// a bobber has no lock, so every client shows the gear. Later clients ship
|
||||
// the hook (Interface\Cursor\Fishing.blp) only for data to name.
|
||||
//
|
||||
// The fishing node handler (type 17) gets its own cursor slot, the way
|
||||
// 4.3.4's guild bank type overrides its cursor. The slot still runs
|
||||
// BaseHandlerGetCursor, which decides everything (0 for a bobber the player
|
||||
// is not channelling, Unable when not usable), and swaps only the image. The
|
||||
// classifier passes the mode to SetCursorMode, whose hook (cursor/Hover)
|
||||
// shows it.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "cursor/Extended.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Cursor::Fishing {
|
||||
|
||||
namespace {
|
||||
|
||||
using HandlerGetCursor_t = uint32_t(__thiscall *)(void *handler);
|
||||
|
||||
uint32_t __fastcall FishingNodeCursor(void *handler, void * /*edx*/) {
|
||||
const uint32_t mode =
|
||||
reinterpret_cast<HandlerGetCursor_t>(Offsets::FUN_GO_HANDLER_BASE_GET_CURSOR)(handler);
|
||||
if (!Cursor::Extended::Enabled())
|
||||
return mode;
|
||||
if (mode == Offsets::CURSOR_MODE_INTERACT)
|
||||
return Cursor::Extended::kFishing;
|
||||
if (mode == Offsets::CURSOR_MODE_INTERACT + Offsets::CURSOR_MODE_UNABLE_OFFSET)
|
||||
return Cursor::Extended::kFishing + Cursor::Extended::kUnableOffset;
|
||||
return mode;
|
||||
}
|
||||
|
||||
void Patch() {
|
||||
Common::RedirectSlot(Offsets::SLOT_FISHING_NODE_HANDLER_GET_CURSOR,
|
||||
Offsets::FUN_GO_HANDLER_BASE_GET_CURSOR,
|
||||
reinterpret_cast<const void *>(&FishingNodeCursor));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Patch};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Cursor::Fishing
|
||||
+23
-1
@@ -27,14 +27,21 @@
|
||||
// The spell-targeting and mouselook legs of 0x4828D0 are left out: they
|
||||
// answer for the mouse, not for the object.
|
||||
//
|
||||
// The unit classifier and the fishing node's handler can also pick a mode of
|
||||
// Cursor::Extended (cursor/Npc, cursor/Fishing). The engine's SetCursorMode
|
||||
// cannot show one, so the same hook hands those to Cursor::Extended::Show,
|
||||
// and while capturing records them like any other.
|
||||
//
|
||||
// The images are the engine's own: `Interface\Cursor\<name>.blp` from its
|
||||
// cursor name table, and a game object's named cursor from its record
|
||||
// (FormatGameObjectCursorFile, `Unable` when not usable).
|
||||
// (FormatGameObjectCursorFile, `Unable` when not usable). An extended mode's
|
||||
// image is its embedded file (Cursor::Extended::Path).
|
||||
|
||||
#include "cursor/Hover.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "cursor/Extended.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
@@ -58,6 +65,10 @@ void __fastcall SetCursorMode_h(uint32_t mode) {
|
||||
g_captured = mode;
|
||||
return;
|
||||
}
|
||||
if (Cursor::Extended::Is(mode)) {
|
||||
Cursor::Extended::Show(mode);
|
||||
return;
|
||||
}
|
||||
SetCursorMode_o(mode);
|
||||
}
|
||||
|
||||
@@ -122,6 +133,17 @@ bool ImagePath(void *object, uint32_t mode, bool lowPriority, char *buf, std::si
|
||||
std::snprintf(buf, size, "%s", file);
|
||||
return true;
|
||||
}
|
||||
if (Cursor::Extended::Is(mode)) {
|
||||
// Retail shows no icon for a quest giver's cursor, not even the
|
||||
// low-priority pointer (checked in game, 2026-11-03, with the soft
|
||||
// interact icon and SoftTargetLowPriorityIcons 1). Blizzard_NamePlates
|
||||
// shows that icon exactly when SetUnitCursorTexture returns true, so
|
||||
// the refusal is the function's own.
|
||||
if (Cursor::Extended::IsQuest(mode))
|
||||
return false;
|
||||
std::snprintf(buf, size, "%s", Cursor::Extended::Path(mode));
|
||||
return true;
|
||||
}
|
||||
if (mode >= Offsets::CURSOR_MODE_CUSTOM)
|
||||
return false;
|
||||
const char *name = Game::Read<const char *>(Offsets::VAR_CURSOR_NAMES + 4 * mode);
|
||||
|
||||
+7
-6
@@ -18,9 +18,9 @@
|
||||
|
||||
namespace Cursor::Hover {
|
||||
|
||||
// The cursor mode (Offsets::CURSOR_MODE_*) the world cursor would show with
|
||||
// the mouse over `object` — a unit, player, game object or corpse — or 0
|
||||
// when it shows none. Computes it without showing it.
|
||||
// The cursor mode (Offsets::CURSOR_MODE_* or Cursor::Extended) the world
|
||||
// cursor would show with the mouse over `object` — a unit, player, game
|
||||
// object or corpse — or 0 when it shows none. Computes it without showing it.
|
||||
uint32_t Mode(void *object);
|
||||
|
||||
// For a game object's named cursor (CURSOR_MODE_CUSTOM): whether the player
|
||||
@@ -28,9 +28,10 @@ uint32_t Mode(void *object);
|
||||
bool IsUsable(void *object);
|
||||
|
||||
// Writes the image file of `mode` over `object` into `buf`. False, with
|
||||
// `buf` untouched, for no mode, and for the base (resting) mode unless
|
||||
// `lowPriority` — retail's low-priority image, the plain pointer over a unit
|
||||
// with nothing to do (SetUnitCursorTexture's includeLowPriority).
|
||||
// `buf` untouched, for no mode, for a quest giver's cursor (retail shows no
|
||||
// icon for one), and for the base (resting) mode unless `lowPriority` —
|
||||
// retail's low-priority image, the plain pointer over a unit with nothing to
|
||||
// do (SetUnitCursorTexture's includeLowPriority).
|
||||
bool ImagePath(void *object, uint32_t mode, bool lowPriority, char *buf, std::size_t size);
|
||||
|
||||
} // namespace Cursor::Hover
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// With `expandedCursors` at 1 (Cursor::Extended::Enabled), the cursor over an
|
||||
// NPC shows what it offers, as in later clients: an anvil for a repairer, a
|
||||
// hearthstone for an innkeeper, a horseshoe for a stable master, and a quest
|
||||
// giver's ! or ? (Cursor::Extended). At 0, the default, it stays 1.12's.
|
||||
//
|
||||
// 1.12 picks the cursor for a hovered unit in ClassifyHoveredUnit
|
||||
// (FUN_WORLD_FRAME_CLASSIFY_UNIT). For a unit the player can interact with,
|
||||
// its NPC flag chain takes the lowest flag bit first: gossip, quest giver,
|
||||
// vendor, flight master and so on. It never tests the repair flag, and shows
|
||||
// Interact for an innkeeper. 4.3.4's CursorTrackUnit first asks the quest
|
||||
// giver's status (GetQuestGiverCursor), then tests the services from repair
|
||||
// down, with vendor and gossip near the end.
|
||||
//
|
||||
// The thunk stands in for the head of 1.12's chain
|
||||
// (SITE_CLASSIFY_UNIT_NPC_FLAGS). Everything the engine decides before it
|
||||
// stays the engine's: mouselook, spell targeting, charm, whether the unit can
|
||||
// be interacted with now, and whether it is beyond the interact reach (EBX,
|
||||
// the Unable form). With the cvar at 0 the thunk does the instruction it
|
||||
// replaced and the stock chain runs. The pick goes to the engine's
|
||||
// SetCursorMode, whose hook (cursor/Hover) shows an extended mode and records
|
||||
// every mode while the soft-interact code asks for the hover verdict.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "cursor/Extended.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Cursor::Npc {
|
||||
|
||||
namespace {
|
||||
|
||||
// UNIT_NPC_FLAGS bits, as 1.12's chain tests them (tortoise-wow
|
||||
// UnitDefines.h names them).
|
||||
constexpr uint32_t kGossip = 0x1;
|
||||
constexpr uint32_t kVendor = 0x4;
|
||||
constexpr uint32_t kFlightMaster = 0x8;
|
||||
constexpr uint32_t kTrainer = 0x10;
|
||||
constexpr uint32_t kSpiritHealer = 0x20;
|
||||
constexpr uint32_t kSpiritGuide = 0x40;
|
||||
constexpr uint32_t kInnkeeper = 0x80;
|
||||
constexpr uint32_t kBanker = 0x100;
|
||||
constexpr uint32_t kPetitioner = 0x200;
|
||||
constexpr uint32_t kTabardDesigner = 0x400;
|
||||
constexpr uint32_t kBattleMaster = 0x800;
|
||||
constexpr uint32_t kAuctioneer = 0x1000;
|
||||
constexpr uint32_t kStableMaster = 0x2000;
|
||||
constexpr uint32_t kRepair = 0x4000;
|
||||
|
||||
// Quest giver statuses (OFF_CGUNIT_QUEST_STATUS). 1.12 numbers them its own
|
||||
// way (tortoise-wow QuestDef.h); its status-to-marker table (0x80C454) shows
|
||||
// each one's meaning: 3 a grey ?, 4 a blue ?, 5 a yellow !, 6 and 7 a yellow
|
||||
// ?. 0 none, 1 unavailable (grey !) and 2 chat (no marker) have no cursor,
|
||||
// as 4.3.4's none and unavailable have none.
|
||||
constexpr uint32_t kStatusIncomplete = 3;
|
||||
constexpr uint32_t kStatusRewardRepeatable = 4;
|
||||
constexpr uint32_t kStatusAvailable = 5;
|
||||
constexpr uint32_t kStatusRewardOld = 6;
|
||||
constexpr uint32_t kStatusReward = 7;
|
||||
|
||||
using IsQuestGiverToTalk_t = uint32_t(__stdcall *)(void *unit);
|
||||
using SetCursorMode_t = void(__fastcall *)(uint32_t mode);
|
||||
|
||||
uintptr_t g_classifyReturn = Offsets::CLASSIFY_UNIT_RETURN;
|
||||
|
||||
// 4.3.4's quest cursor for a status (GetQuestGiverCursor), matched by the
|
||||
// status's meaning: 4.3.4 gives Quest to an available and to an incomplete
|
||||
// quest, QuestRepeatable to a repeatable reward (its blue ?), and
|
||||
// QuestTurnIn to a reward (its yellow ?).
|
||||
uint32_t QuestMode(uint32_t status) {
|
||||
switch (status) {
|
||||
case kStatusIncomplete:
|
||||
case kStatusAvailable:
|
||||
return Cursor::Extended::kQuest;
|
||||
case kStatusRewardRepeatable:
|
||||
return Cursor::Extended::kQuestRepeatable;
|
||||
case kStatusRewardOld:
|
||||
case kStatusReward:
|
||||
return Cursor::Extended::kQuestTurnIn;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 4.3.4's order (CursorTrackUnit) in 1.12's flag bits, or 0 for none.
|
||||
// 4.3.4's guild master, guild banker and spell click tests have no 1.12
|
||||
// flag, and its flight master test also reads an icon type; for that flag
|
||||
// 1.12 shows Taxi, as here. 4.3.4 also takes a cursor that the creature's
|
||||
// cache record names (IconName), which 1.12's creature query does not carry.
|
||||
// A stable master gets retail's StableMaster in 4.3.4's place for it, where
|
||||
// 4.3.4 shows Speak. Where 4.3.4 tests gossip, this also takes the quest
|
||||
// giver 1.12 would speak to (IsQuestGiverToTalk), so a status without a
|
||||
// quest cursor keeps 1.12's Speak.
|
||||
uint32_t ServiceMode(void *unit) {
|
||||
if (const uint32_t quest = QuestMode(Game::Read<uint32_t>(unit, Offsets::OFF_CGUNIT_QUEST_STATUS)))
|
||||
return quest;
|
||||
const uint32_t flags =
|
||||
Game::Read<uint32_t>(Game::Read<const void *>(unit, Offsets::OFF_UNIT_DESCRIPTOR),
|
||||
Offsets::OFF_UNIT_DESCRIPTOR_NPC_FLAGS);
|
||||
if (flags & kRepair)
|
||||
return Cursor::Extended::kRepairNpc;
|
||||
if (flags & kInnkeeper)
|
||||
return Cursor::Extended::kInnkeeper;
|
||||
if (flags & kFlightMaster)
|
||||
return Offsets::CURSOR_MODE_TAXI;
|
||||
if (flags & kTrainer)
|
||||
return Offsets::CURSOR_MODE_TRAINER;
|
||||
if (flags & (kSpiritHealer | kSpiritGuide))
|
||||
return Offsets::CURSOR_MODE_SPEAK;
|
||||
if (flags & kBanker)
|
||||
return Offsets::CURSOR_MODE_BUY;
|
||||
if (flags & (kPetitioner | kTabardDesigner | kBattleMaster))
|
||||
return Offsets::CURSOR_MODE_SPEAK;
|
||||
if (flags & kAuctioneer)
|
||||
return Offsets::CURSOR_MODE_BUY;
|
||||
if (flags & kStableMaster)
|
||||
return Cursor::Extended::kStableMaster;
|
||||
if (flags & kVendor)
|
||||
return Offsets::CURSOR_MODE_PICKUP;
|
||||
if ((flags & kGossip) ||
|
||||
reinterpret_cast<IsQuestGiverToTalk_t>(Offsets::FUN_IS_QUEST_GIVER_TO_TALK)(unit) != 0)
|
||||
return Offsets::CURSOR_MODE_SPEAK;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// True when it set the mode. `unable` is the engine's EBX: 1 beyond the
|
||||
// interact reach, else 0. A unit with no service gets the base mode, where
|
||||
// the stock chain ends too (ShowBaseCursor).
|
||||
bool __stdcall Pick(void *unit, uint32_t unable) {
|
||||
if (!Cursor::Extended::Enabled())
|
||||
return false;
|
||||
uint32_t mode = ServiceMode(unit);
|
||||
if (mode == 0)
|
||||
mode = Game::Read<uint32_t>(Offsets::VAR_CURSOR_BASE_MODE);
|
||||
else if (unable != 0)
|
||||
mode += Cursor::Extended::Is(mode) ? Cursor::Extended::kUnableOffset
|
||||
: Offsets::CURSOR_MODE_UNABLE_OFFSET;
|
||||
reinterpret_cast<SetCursorMode_t>(Offsets::FUN_SET_CURSOR_MODE)(mode);
|
||||
return true;
|
||||
}
|
||||
|
||||
static_assert(Offsets::OFF_UNIT_DESCRIPTOR == 0x110, "NpcFlagsThunk replays [esi + 0x110]");
|
||||
|
||||
// Stands in for `mov ecx, [esi + 0x110]`, the head of the chain: the unit in
|
||||
// ESI, the Unable flag in EBX. When Pick set the mode, it leaves the
|
||||
// classifier through the chain's epilogue; otherwise it does the instruction
|
||||
// it replaced and the chain runs.
|
||||
__declspec(naked) void NpcFlagsThunk() {
|
||||
__asm {
|
||||
push ebx
|
||||
push esi
|
||||
call Pick
|
||||
test al, al
|
||||
jnz picked
|
||||
mov ecx, dword ptr [esi + 110h]
|
||||
ret
|
||||
picked:
|
||||
add esp, 4
|
||||
jmp dword ptr [g_classifyReturn]
|
||||
}
|
||||
}
|
||||
|
||||
void Patch() {
|
||||
static const uint8_t kFlagsRead[6] = {0x8B, 0x8E, 0x10, 0x01, 0x00, 0x00};
|
||||
Common::ReplaceWithCall(Offsets::SITE_CLASSIFY_UNIT_NPC_FLAGS, kFlagsRead, sizeof kFlagsRead,
|
||||
reinterpret_cast<const void *>(&NpcFlagsThunk));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Patch};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Cursor::Npc
|
||||
+5
-3
@@ -25,11 +25,13 @@ const char *NormalizeEmpty(const char *s) {
|
||||
|
||||
} // namespace
|
||||
|
||||
// The id is compared unsigned: a DBC column's "none" is -1, which a signed
|
||||
// compare passes as less than the count and then indexes records[-1]. The
|
||||
// engine rejects it with a signed `jl` before its `jg` against the count
|
||||
// (0x00472797). An unloaded store's count is -1, so it takes no id.
|
||||
const uint8_t *Record(uintptr_t recordsVar, uintptr_t countVar, uint32_t id) {
|
||||
if (id == 0)
|
||||
return nullptr;
|
||||
const int count = *reinterpret_cast<const int *>(countVar);
|
||||
if (static_cast<int>(id) > count)
|
||||
if (id == 0 || count <= 0 || id > static_cast<uint32_t>(count))
|
||||
return nullptr;
|
||||
const uint8_t *const *records =
|
||||
*reinterpret_cast<const uint8_t *const *const *>(recordsVar);
|
||||
|
||||
+2
-1
@@ -39,7 +39,8 @@ namespace DBC {
|
||||
|
||||
// Resolves `id` to its record pointer in the table whose records-base
|
||||
// global is at `recordsVar` and count global at `countVar`. Returns
|
||||
// `nullptr` for `id == 0`, OOR ids (> count), and unpopulated slots.
|
||||
// `nullptr` for `id == 0`, OOR ids (> count, which includes -1 and every
|
||||
// other negative id), and unpopulated slots.
|
||||
// Caller reads whatever fields it needs off the returned record.
|
||||
//
|
||||
// Use this when `LocalizedField` / `StringField` don't fit — e.g. for
|
||||
|
||||
@@ -206,9 +206,8 @@ void RegisterLuaFunctions() {
|
||||
&Script_GetSelfResurrectOptions, &kGetSelfResurrectOptions);
|
||||
Game::Lua::RegisterTableFunction("C_DeathInfo", "UseSelfResurrectOption",
|
||||
&Script_UseSelfResurrectOption, &kUseSelfResurrectOption);
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "SelfResurrectOptionType", kOptionTypeEntries,
|
||||
sizeof(kOptionTypeEntries) / sizeof(kOptionTypeEntries[0]),
|
||||
"DeathInfo");
|
||||
Game::Lua::RegisterIntegerEnum(
|
||||
"Enum", "SelfResurrectOptionType", kOptionTypeEntries, "DeathInfo");
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -47,7 +47,6 @@
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <iterator>
|
||||
|
||||
namespace Duration::Object {
|
||||
|
||||
@@ -451,8 +450,7 @@ const Game::Doc::Function kCreateDuration{"Creates a zero duration object that c
|
||||
void RegisterLuaFunctions() {
|
||||
CreateMetatable(Lua::State());
|
||||
Lua::RegisterTableFunction("C_DurationUtil", "CreateDuration", &Script_CreateDuration, &kCreateDuration);
|
||||
Lua::RegisterIntegerEnum("Enum", "DurationTimeModifier", kModifiers, static_cast<int>(std::size(kModifiers)),
|
||||
"LuaDurationObjectShared");
|
||||
Lua::RegisterIntegerEnum("Enum", "DurationTimeModifier", kModifiers, "LuaDurationObjectShared");
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -58,7 +58,6 @@
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -780,8 +779,8 @@ void RegisterLuaFunctions() {
|
||||
CreateMetatable(Lua::State());
|
||||
Lua::RegisterTableFunction("C_DurationUtil", "CreateDurationTextBinding", &Script_CreateDurationTextBinding,
|
||||
&kCreateDurationTextBinding);
|
||||
Lua::RegisterIntegerEnum("Enum", "DurationTextBindingProperty", kProperties,
|
||||
static_cast<int>(std::size(kProperties)), "DurationTextBindingShared");
|
||||
Lua::RegisterIntegerEnum(
|
||||
"Enum", "DurationTextBindingProperty", kProperties, "DurationTextBindingShared");
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -125,8 +125,8 @@ const Game::Lua::FrameMethodEntry g_methods[] = {
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_EDITBOX_METHOD_REGISTRY), g_methods,
|
||||
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])));
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_EDITBOX_METHOD_REGISTRY), g_methods);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -83,9 +83,7 @@ const Game::Doc::Method g_methodDocs[] = {
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_EDITBOX_METHOD_REGISTRY), g_methods,
|
||||
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
|
||||
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
|
||||
reinterpret_cast<void *>(Offsets::VAR_EDITBOX_METHOD_REGISTRY), g_methods, g_methodDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "frame/Layout.h"
|
||||
#include "text/InlineTexture.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -59,21 +60,17 @@ using IndexAtPoint_t = void(__fastcall *)(void *editBox, void *edx, float x, flo
|
||||
using FontHeight_t = float(__fastcall *)(void *fs, void *edx, int mode);
|
||||
using MeasureRun_t = float(__fastcall *)(void *fs, void *edx, const uint8_t *text, int len);
|
||||
using PixelAlignY_t = float(__stdcall *)(float length);
|
||||
using GetRect_t = int(__fastcall *)(void *anchor, void *edx, float *out);
|
||||
using ClearAllPoints_t = void(__fastcall *)(void *anchor);
|
||||
using SetPoint_t = void(__thiscall *)(void *anchor, int point, void *relAnchor, int relPoint, float x, float y,
|
||||
int flag);
|
||||
using Resize_t = void(__fastcall *)(void *anchor, void *edx, int force);
|
||||
using Extent_t = float(__fastcall *)(void *anchor, void *edx);
|
||||
using ScreenAspect_t = float(__cdecl *)();
|
||||
using DivideByScreenScaleX_t = float(__stdcall *)(float value);
|
||||
using FireVariadic_t = void(__cdecl *)(void *frame, uint32_t *slotPtr, const char *fmt, ...);
|
||||
|
||||
PlaceCaret_t g_origPlaceCaret = nullptr;
|
||||
PlaceSelectionPiece_t g_origPlaceSelectionPiece = nullptr;
|
||||
IndexAtPoint_t g_origIndexAtPoint = nullptr;
|
||||
|
||||
void *AnchorOf(void *region) { return static_cast<uint8_t *>(region) + Offsets::OFF_REGION_ANCHOR; }
|
||||
using Frame::Layout::Of;
|
||||
|
||||
// A multi-line box's rows: its label, the buffer the line table indexes (the
|
||||
// '*' copy of a password box) and the table — each line's first byte, the
|
||||
@@ -179,9 +176,9 @@ bool AnchorX(const Rows &rows, int line, uint32_t pos, float &x, int &relPoint)
|
||||
|
||||
// Anchors `region` as the placer did, at `y`.
|
||||
void Anchor(const Rows &rows, void *region, int relPoint, float x, float y, int flag) {
|
||||
void *anchor = AnchorOf(region);
|
||||
void *anchor = Of(region);
|
||||
reinterpret_cast<ClearAllPoints_t>(Offsets::FUN_LAYOUT_CLEAR_ALL_POINTS)(anchor);
|
||||
reinterpret_cast<SetPoint_t>(Offsets::FUN_REGION_SET_POINT)(anchor, Offsets::FRAMEPOINT_TOPLEFT, AnchorOf(rows.fs),
|
||||
reinterpret_cast<SetPoint_t>(Offsets::FUN_REGION_SET_POINT)(anchor, Offsets::FRAMEPOINT_TOPLEFT, Of(rows.fs),
|
||||
relPoint, x, y, flag);
|
||||
}
|
||||
|
||||
@@ -192,7 +189,7 @@ void __fastcall IndexAtPoint_h(void *editBox, void *edx, float x, float y, int *
|
||||
if (!MultiLineRows(editBox, rows))
|
||||
return g_origIndexAtPoint(editBox, edx, x, y, index);
|
||||
float rect[4] = {0.0f, 0.0f, 0.0f, 0.0f}; // bottom, left, top, right
|
||||
if (reinterpret_cast<GetRect_t>(Offsets::FUN_REGION_GET_RECT)(AnchorOf(rows.fs), nullptr, rect) == 0)
|
||||
if (!Frame::Layout::GetRect(rows.fs, rect))
|
||||
return g_origIndexAtPoint(editBox, edx, x, y, index);
|
||||
// The engine walks rows down from the label's top while the click is
|
||||
// below the row (strictly), stopping at the last line. Walk the rows as
|
||||
@@ -240,7 +237,7 @@ void __fastcall PlaceSelectionPiece_h(void *editBox, void *edx, void *piece, int
|
||||
return;
|
||||
const float y = -(RowHeight(rows) / rows.scale) * static_cast<float>(line) - shift;
|
||||
Anchor(rows, piece, relPoint, x, y, 0);
|
||||
reinterpret_cast<Resize_t>(Offsets::FUN_LAYOUT_RESIZE)(AnchorOf(piece), nullptr, 1);
|
||||
Frame::Layout::Resize(piece, true);
|
||||
}
|
||||
|
||||
// --- the caret ----------------------------------------------------------------
|
||||
@@ -276,15 +273,12 @@ void __fastcall PlaceCaret_h(void *editBox) {
|
||||
return;
|
||||
// OnCursorChanged(x, y, width, height) as the engine fires it: each
|
||||
// value by the anchor→script factor, pushed as a double.
|
||||
void *anchor = AnchorOf(caret);
|
||||
void *anchor = Of(caret);
|
||||
void **vtable = *static_cast<void ***>(anchor);
|
||||
const float width = reinterpret_cast<Extent_t>(vtable[7])(anchor, nullptr); // +0x1C GetWidth
|
||||
const float height = reinterpret_cast<Extent_t>(vtable[8])(anchor, nullptr); // +0x20 GetHeight
|
||||
const auto aspect = reinterpret_cast<ScreenAspect_t>(Offsets::FUN_COORD_GET_SCALED_SCREEN_ASPECT);
|
||||
const auto divide = reinterpret_cast<DivideByScreenScaleX_t>(Offsets::FUN_COORD_DIVIDE_BY_SCREEN_SCALE_X);
|
||||
const float factor = Game::Read<float>(Offsets::FLOAT_SCRIPT_COORD_SCALE);
|
||||
const auto push = [&](float value) { return static_cast<double>(divide(aspect() * factor * value)); };
|
||||
const double args[4] = {push(x), push(y), push(width), push(height)};
|
||||
const double args[4] = {Game::InternalToPixel(x), Game::InternalToPixel(y), Game::InternalToPixel(width),
|
||||
Game::InternalToPixel(height)};
|
||||
void *L = Game::Lua::State();
|
||||
const int savedTop = (L != nullptr) ? Game::Lua::GetTop(L) : 0;
|
||||
reinterpret_cast<FireVariadic_t>(Offsets::FUN_FRAME_RUN_SCRIPT_VARIADIC)(
|
||||
|
||||
+1
-3
@@ -86,9 +86,7 @@ void RegisterLuaFunctions() {
|
||||
Common::RedirectCall(Offsets::CALL_EDITBOX_IME_COMPOSITION_INSERT, Offsets::FUN_EDITBOX_INSERT,
|
||||
reinterpret_cast<const void *>(&CompositionInsert));
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_EDITBOX_METHOD_REGISTRY), g_methods,
|
||||
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
|
||||
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
|
||||
reinterpret_cast<void *>(Offsets::VAR_EDITBOX_METHOD_REGISTRY), g_methods, g_methodDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -30,8 +30,7 @@ const Game::Lua::EnumIntegerEntry kEntries[] = {
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "InputContext", kEntries,
|
||||
sizeof(kEntries) / sizeof(kEntries[0]), "InputConstants");
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "InputContext", kEntries, "InputConstants");
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -234,9 +234,7 @@ const Game::Lua::FrameMethodEntry g_methods[] = {
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_EDITBOX_METHOD_REGISTRY),
|
||||
g_methods,
|
||||
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])));
|
||||
reinterpret_cast<void *>(Offsets::VAR_EDITBOX_METHOD_REGISTRY), g_methods);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
## Interface: 11200
|
||||
## Title: !!!ClassicAPI
|
||||
## Notes: Library to emulate aspects of the modern WoW API on 1.12.1 / Lua 5.0.
|
||||
## Notes-ruRU: Библиотека, эмулирующая аспекты современного API WoW на 1.12.1 / Lua 5.0.
|
||||
## Version: DEV
|
||||
## Author: Brues
|
||||
## X-Website: https://github.com/brues-code/ClassicAPI
|
||||
@@ -143,6 +144,18 @@ FrameXML\ColorSwatch.lua
|
||||
FrameXML\ColorSwatch.xml
|
||||
FrameXML\ScriptErrorsFrame.lua
|
||||
FrameXML\ScriptErrorsFrame.xml
|
||||
FrameXML\ModelScene\CameraRegistry.lua
|
||||
FrameXML\ModelScene\CameraBaseMixin.lua
|
||||
FrameXML\ModelScene\OrbitCameraMixin.lua
|
||||
FrameXML\ModelScene\FollowCameraMixin.lua
|
||||
FrameXML\ModelScene\ModelSceneMixin.lua
|
||||
FrameXML\ModelScene\ModelSceneMixin.xml
|
||||
FrameXML\ModelScene\ModelSceneActorMixin.lua
|
||||
FrameXML\ModelScene\ModelSceneActorMixin.xml
|
||||
FrameXML\ModelScene\ModelSceneControlFrame.lua
|
||||
FrameXML\ModelScene\ModelSceneControlFrame.xml
|
||||
FrameXML\DressUpFrame.xml
|
||||
FrameXML\DressUpFrame.lua
|
||||
|
||||
Menu\MenuConstants.lua
|
||||
Menu\Classic\MenuConstants.lua
|
||||
|
||||
@@ -24,3 +24,22 @@ end
|
||||
function C_AutoComplete.GetAutoCompleteRealms()
|
||||
return {}
|
||||
end
|
||||
|
||||
-- C_ModelInfo's getters come from the DLL. Retail's active-scene bookkeeping
|
||||
-- "does nothing in public clients" (its own documentation).
|
||||
function C_ModelInfo.AddActiveModelScene(modelSceneFrame, modelSceneID)
|
||||
end
|
||||
|
||||
function C_ModelInfo.AddActiveModelSceneActor(modelSceneFrameActor, modelSceneActorID)
|
||||
end
|
||||
|
||||
function C_ModelInfo.ClearActiveModelScene(modelSceneFrame)
|
||||
end
|
||||
|
||||
function C_ModelInfo.ClearActiveModelSceneActor(modelSceneFrameActor)
|
||||
end
|
||||
|
||||
-- This client has no alternate player forms.
|
||||
function C_PlayerInfo.GetAlternateFormInfo()
|
||||
return false, false
|
||||
end
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
-- 4.3.4's DressUpItemLink (FrameXML/ModelFrames.lua): the whole link goes to
|
||||
-- the model, so DressUpModel:TryOn shows the link's enchant glow, and an item
|
||||
-- the model cannot wear does not open the window. 1.12.1's version passes
|
||||
-- only the item ID.
|
||||
--
|
||||
-- Differences from the 4.3.4 source:
|
||||
-- - The link goes through DressUpItem, which 4.3.4 folded in. Here
|
||||
-- Blizzard_AuctionUI replaces DressUpItem to show its own model, and
|
||||
-- some clients add steps to it (Turtle dresses the tabard).
|
||||
-- - An uncached item skips the IsDressableItem test, which reads only the
|
||||
-- cache. 1.12.1 opens no tooltip before a chat link's Control-click, so
|
||||
-- the test would drop most chat links; TryOn loads the item instead.
|
||||
-- - An item the model cannot wear goes on to DressUpMountLink, retail's
|
||||
-- next step in DressUpLink: 1.12.1's FrameXML (ItemRef, bags, loot, the
|
||||
-- auction house) calls DressUpItemLink alone.
|
||||
-- - It returns true when it showed the item or its mount, and false for a
|
||||
-- link of another type, as retail's does.
|
||||
|
||||
-- A link DressUpItemLink takes: an item link, or anything that is not a
|
||||
-- link (an item ID or name).
|
||||
local function IsItemLink(link)
|
||||
local linkType = type(link) == "string" and LinkUtil.ExtractLink(link);
|
||||
return not linkType or linkType == "item";
|
||||
end
|
||||
|
||||
-- 4.3.4's DressUpItemLink body: the item, when the model can wear it.
|
||||
local function DressUpWearable(link)
|
||||
if ( C_Item.IsItemDataCachedByID(link) and not IsDressableItem(link) ) then
|
||||
return false;
|
||||
end
|
||||
DressUpItem(link);
|
||||
return true;
|
||||
end
|
||||
|
||||
function DressUpItemLink(link)
|
||||
if ( not link or not IsItemLink(link) ) then
|
||||
return false;
|
||||
end
|
||||
return DressUpWearable(link) or DressUpMountLink(link);
|
||||
end
|
||||
|
||||
-- Retail's DressUpLink (Mainline/DressUpFrames.lua), without battle pets.
|
||||
function DressUpLink(link, forcedFrame)
|
||||
return link and ((IsItemLink(link) and DressUpWearable(link)) or DressUpMountLink(link, forcedFrame));
|
||||
end
|
||||
|
||||
-- Retail's mount preview (Mainline/DressUpFrames.lua DressUpMountLink and
|
||||
-- DressUpMount, Blizzard_SharedXMLGame/DressUpModelFrameMixin.lua's mode and
|
||||
-- last link) on 1.12.1's dressing room. Mount mode shows DressUpFrameModelScene
|
||||
-- in place of DressUpModel and hides the Reset button, as retail's SetMode
|
||||
-- hides it outside player mode; DressUpItem goes back to player mode.
|
||||
--
|
||||
-- Differences from the retail source:
|
||||
-- - DressUpFrame keeps its own background (retail switches to the warrior
|
||||
-- one) and has no view insets to set (retail sets them to 0, the default).
|
||||
-- - There are no mount links, so a link never sets the model itself, and
|
||||
-- shouldSetModelFromHyperlink changes nothing.
|
||||
-- - A mount whose creature is not cached yet returns false, as retail does
|
||||
-- with no display, and opens when CREATURE_DATA_LOAD_RESULT reports the
|
||||
-- creature: retail has every display on disk, this client asks the server.
|
||||
|
||||
DressUpFrame.ModelScene = DressUpFrameModelScene;
|
||||
DressUpFrame.ModelScene.ControlFrame:SetModelScene(DressUpFrame.ModelScene);
|
||||
|
||||
function DressUpFrame:GetLastLink()
|
||||
return self.lastLink;
|
||||
end
|
||||
|
||||
function DressUpFrame:SetLastLink(link)
|
||||
self.lastLink = link;
|
||||
end
|
||||
|
||||
function DressUpFrame:GetMode()
|
||||
return self.mode;
|
||||
end
|
||||
|
||||
function DressUpFrame:SetMode(mode)
|
||||
self.mode = mode;
|
||||
local inPlayerMode = mode ~= "mount";
|
||||
DressUpModel:SetShown(inPlayerMode);
|
||||
DressUpFrameResetButton:SetShown(inPlayerMode);
|
||||
self.ModelScene:SetShown(not inPlayerMode);
|
||||
end
|
||||
|
||||
DressUpFrame.ModelScene:SetResetCallback(function()
|
||||
local link = DressUpFrame:GetLastLink();
|
||||
if link then
|
||||
DressUpLink(link, DressUpFrame);
|
||||
end
|
||||
end);
|
||||
|
||||
-- The stock DressUpItem sets the unit only when it opens the window, and the
|
||||
-- model was hidden while a mount showed.
|
||||
local originalDressUpItem = DressUpItem;
|
||||
function DressUpItem(item)
|
||||
if ( DressUpFrame:GetMode() == "mount" ) then
|
||||
DressUpFrame:SetMode("player");
|
||||
DressUpFrame:SetLastLink(nil);
|
||||
if ( DressUpFrame:IsVisible() ) then
|
||||
DressUpModel:SetUnit("player");
|
||||
end
|
||||
end
|
||||
return originalDressUpItem(item);
|
||||
end
|
||||
|
||||
function DressUpMountLink(link, forcedFrame)
|
||||
if( link ) then
|
||||
local mountID = 0;
|
||||
|
||||
local linkType, linkOptions = LinkUtil.ExtractLink(link);
|
||||
if linkType == "item" then
|
||||
local linkID = LinkUtil.SplitLinkOptions(linkOptions);
|
||||
mountID = C_MountJournal.GetMountFromItem(tonumber(linkID));
|
||||
elseif linkType == "spell" then
|
||||
local linkID = LinkUtil.SplitLinkOptions(linkOptions);
|
||||
mountID = C_MountJournal.GetMountFromSpell(tonumber(linkID));
|
||||
end
|
||||
|
||||
if ( mountID ) then
|
||||
return DressUpMount(mountID, forcedFrame, false, link);
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local pendingMount;
|
||||
|
||||
-- The creature GetMountInfoExtraByID loads for a mount: a mount ID is its
|
||||
-- spell ID, and the mount aura's misc value is the creature.
|
||||
local SPELL_AURA_MOUNTED = 78;
|
||||
local function MountCreatureID(mountID)
|
||||
local effects = C_Spell.GetSpellEffectInfo(mountID);
|
||||
if ( effects ) then
|
||||
for _, effect in ipairs(effects) do
|
||||
if ( effect.auraName == SPELL_AURA_MOUNTED ) then
|
||||
return effect.miscValue;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function DressUpMount(mountID, forcedFrame, shouldSetModelFromHyperlink, link)
|
||||
if ( not mountID or mountID == 0 ) then
|
||||
return false;
|
||||
end
|
||||
|
||||
local creatureDisplayID, _, _, isSelfMount, _, modelSceneID, animID, spellVisualKitID, disablePlayerMountPreview = C_MountJournal.GetMountInfoExtraByID(mountID);
|
||||
if ( not creatureDisplayID ) then
|
||||
pendingMount = { mountID = mountID, creatureID = MountCreatureID(mountID), forcedFrame = forcedFrame, link = link };
|
||||
return false;
|
||||
end
|
||||
pendingMount = nil;
|
||||
|
||||
local frame = forcedFrame or DressUpFrame;
|
||||
|
||||
--Show the frame
|
||||
if ( not frame:IsShown() or frame:GetMode() ~= "mount" ) then
|
||||
frame:SetMode("mount");
|
||||
ShowUIPanel(frame);
|
||||
end
|
||||
frame:SetLastLink(link);
|
||||
frame.ModelScene:ClearScene();
|
||||
local forceEvenIfSame = true;
|
||||
frame.ModelScene:TransitionToModelSceneID(modelSceneID, CAMERA_TRANSITION_TYPE_IMMEDIATE, CAMERA_MODIFICATION_TYPE_DISCARD, forceEvenIfSame);
|
||||
|
||||
local mountActor = frame.ModelScene:GetActorByTag("unwrapped");
|
||||
if mountActor then
|
||||
mountActor:SetModelByCreatureDisplayID(creatureDisplayID);
|
||||
|
||||
-- mount self idle animation
|
||||
if (isSelfMount) then
|
||||
mountActor:SetAnimationBlendOperation(Enum.ModelBlendOperation.None);
|
||||
mountActor:SetAnimation(618); -- MountSelfIdle
|
||||
else
|
||||
mountActor:SetAnimationBlendOperation(Enum.ModelBlendOperation.Anim);
|
||||
mountActor:SetAnimation(0);
|
||||
end
|
||||
frame.ModelScene:AttachPlayerToMount(mountActor, animID, isSelfMount, disablePlayerMountPreview);
|
||||
end
|
||||
return true;
|
||||
end
|
||||
|
||||
local creatureLoadFrame = CreateFrame("Frame");
|
||||
creatureLoadFrame:RegisterEvent("CREATURE_DATA_LOAD_RESULT");
|
||||
creatureLoadFrame:SetScript("OnEvent", function(self, event, creatureID, success)
|
||||
local pending = pendingMount;
|
||||
if ( not pending or creatureID ~= pending.creatureID ) then
|
||||
return;
|
||||
end
|
||||
pendingMount = nil;
|
||||
if ( success ) then
|
||||
DressUpMount(pending.mountID, pending.forcedFrame, false, pending.link);
|
||||
end
|
||||
end);
|
||||
|
||||
-- Control-click on a mount in the spellbook previews it. Retail has no such
|
||||
-- click (its mounts are in the mount journal); this client keeps mounts in
|
||||
-- the spellbook, where stock 1.12.1 casts on a Control-click. A mount spell
|
||||
-- never casts here, even while its creature loads; any other spell does.
|
||||
local originalSpellButtonOnClick = SpellButton_OnClick;
|
||||
function SpellButton_OnClick(drag)
|
||||
if ( not drag and IsControlKeyDown() and SpellBookFrame.bookType ~= BOOKTYPE_PET ) then
|
||||
local id = SpellBook_GetSpellID(this:GetID());
|
||||
local link = id <= MAX_SPELLS and GetSpellLink(id, SpellBookFrame.bookType);
|
||||
if ( link and C_MountJournal.GetMountFromSpell(link) ) then
|
||||
this:SetChecked("false");
|
||||
DressUpMountLink(link);
|
||||
return;
|
||||
end
|
||||
end
|
||||
return originalSpellButtonOnClick(drag);
|
||||
end
|
||||
@@ -0,0 +1,27 @@
|
||||
<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">
|
||||
<!-- The dressing room's mount mode (DressUpFrame.lua): a scene shown in
|
||||
place of DressUpModel, with retail's control frame. Like retail's, it
|
||||
fills the backdrop above the buttons: the four DressUpBackground
|
||||
textures, cut off at the top of the Reset / Close row (centred 422
|
||||
below the frame top, 22 tall, so 48 above the backdrop's bottom).
|
||||
DressUpModel's own rect is no guide: it is a scale 2 frame that
|
||||
reaches past the backdrop and over the buttons. -->
|
||||
<ModelScene name="DressUpFrameModelScene" parent="DressUpFrame" inherits="ModelSceneMixinTemplate" hidden="true">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeTo="DressUpBackgroundTopLeft"/>
|
||||
<Anchor point="BOTTOMRIGHT" relativeTo="DressUpBackgroundBotRight">
|
||||
<Offset>
|
||||
<AbsDimension x="0" y="48"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Frames>
|
||||
<Frame parentKey="ControlFrame" inherits="ModelSceneControlFrameTemplate">
|
||||
<Anchors>
|
||||
<Anchor point="TOP" x="0" y="-10"/>
|
||||
</Anchors>
|
||||
</Frame>
|
||||
</Frames>
|
||||
</ModelScene>
|
||||
</Ui>
|
||||
@@ -0,0 +1,103 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/Shared/ModelSceneCameras/CameraBaseMixin.lua.
|
||||
|
||||
|
||||
CameraBaseMixin = {};
|
||||
|
||||
function CameraBaseMixin:SetOwningScene(owningScene)
|
||||
if self.owningScene ~= owningScene then
|
||||
if self.owningScene then
|
||||
self:OnRemoved();
|
||||
end
|
||||
|
||||
self.owningScene = owningScene;
|
||||
|
||||
if self.owningScene then
|
||||
self:OnAdded();
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function CameraBaseMixin:GetOwningScene()
|
||||
return self.owningScene;
|
||||
end
|
||||
|
||||
CAMERA_TRANSITION_TYPE_IMMEDIATE = 1; -- Switch to the camera view instantly
|
||||
CAMERA_TRANSITION_TYPE_INTERPOLATION = 2; -- Switch to the camera view by interpolating from the current view
|
||||
|
||||
CAMERA_MODIFICATION_TYPE_DISCARD = 1; -- Discard any modifications performed to the view by script
|
||||
CAMERA_MODIFICATION_TYPE_MAINTAIN = 2; -- Retain any modifications performed to the view by script
|
||||
|
||||
-- Override this to handle transitions to a modelSceneCamera data set
|
||||
function CameraBaseMixin:ApplyFromModelSceneCameraInfo(modelSceneCameraInfo, transitionType, modificationType)
|
||||
|
||||
end
|
||||
|
||||
-- Common accessors from the owning scene
|
||||
function CameraBaseMixin:IsLeftMouseButtonDown()
|
||||
return self:GetOwningScene():IsLeftMouseButtonDown();
|
||||
end
|
||||
|
||||
function CameraBaseMixin:IsRightMouseButtonDown()
|
||||
return self:GetOwningScene():IsRightMouseButtonDown();
|
||||
end
|
||||
|
||||
function CameraBaseMixin:SetPosition(x, y, z)
|
||||
self:GetOwningScene():SetCameraPosition(x, y, z);
|
||||
end
|
||||
|
||||
function CameraBaseMixin:GetPosition()
|
||||
return self:GetOwningScene():GetCameraPosition();
|
||||
end
|
||||
|
||||
function CameraBaseMixin:GetForwardVector()
|
||||
return self:GetOwningScene():GetCameraForward();
|
||||
end
|
||||
|
||||
function CameraBaseMixin:GetRightVector()
|
||||
return self:GetOwningScene():GetCameraRight();
|
||||
end
|
||||
|
||||
function CameraBaseMixin:GetUpVector()
|
||||
return self:GetOwningScene():GetCameraUp();
|
||||
end
|
||||
|
||||
-- Override this with a uniquely identifying name for the camera type
|
||||
function CameraBaseMixin:GetCameraType()
|
||||
return "UnknownCamera";
|
||||
end
|
||||
|
||||
-- Override this to perform any per-frame updates
|
||||
function CameraBaseMixin:OnUpdate(elapsed)
|
||||
end
|
||||
|
||||
-- Override this with code to synchronize the camera state with the model scene API, may be called by the ModelScene to synchronize deferred camera state in case it needs to be queried
|
||||
function CameraBaseMixin:SynchronizeCamera()
|
||||
end
|
||||
|
||||
-- Override this to be notified when the mouse is down on the model scene
|
||||
function CameraBaseMixin:OnMouseDown(button)
|
||||
end
|
||||
|
||||
-- Override this to be notified when the mouse is up on the model scene
|
||||
function CameraBaseMixin:OnMouseUp(button)
|
||||
end
|
||||
|
||||
-- Override this to be notified when the mouse wheel is changed on the model scene
|
||||
function CameraBaseMixin:OnMouseWheel(delta)
|
||||
end
|
||||
|
||||
-- Override this to be notified when the camera is added to a scene
|
||||
function CameraBaseMixin:OnAdded()
|
||||
end
|
||||
|
||||
-- Override this to be notified when the camera is remove from a scene
|
||||
function CameraBaseMixin:OnRemoved()
|
||||
end
|
||||
|
||||
-- Override this to be notified when the camera becomes activated
|
||||
function CameraBaseMixin:OnActivated()
|
||||
end
|
||||
|
||||
-- Override this to be notified when the camera becomes deactivated
|
||||
function CameraBaseMixin:OnDeactivated()
|
||||
end
|
||||
@@ -0,0 +1,18 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/Shared/ModelSceneCameras/CameraRegistry.lua.
|
||||
|
||||
|
||||
CameraRegistry = { cameraTypeToFactoryFunction = {}, };
|
||||
|
||||
function CameraRegistry:AddCameraFactory(cameraTypeName, factoryFunction)
|
||||
self.cameraTypeToFactoryFunction[cameraTypeName] = factoryFunction;
|
||||
end
|
||||
|
||||
function CameraRegistry:AddCameraFactoryFromMixin(cameraTypeName, mixin)
|
||||
self:AddCameraFactory(cameraTypeName, function() return CreateFromMixins(mixin); end);
|
||||
end
|
||||
|
||||
function CameraRegistry:CreateCameraByType(cameraTypeName)
|
||||
if self.cameraTypeToFactoryFunction[cameraTypeName] then
|
||||
return self.cameraTypeToFactoryFunction[cameraTypeName]();
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,133 @@
|
||||
-- ClassicAPI extension: an orbit camera that follows an actor and stops in
|
||||
-- front of the WMO between the actor and the eye.
|
||||
|
||||
--[[
|
||||
FollowCameraMixin - An OrbitCamera whose target is an actor's pivot: its position, raised by the pivot height.
|
||||
|
||||
The pivot height is the one the world's follow camera eases to for a unit (actor:GetCameraPivotHeight), and
|
||||
it eases the same way: the camera's first height is taken at once (BeginHeightTransition 0x00512790), and
|
||||
each new one is armed (ArmHeight 0x005126B0) for |change| / cameraHeightSmoothSpeed seconds and eased with
|
||||
the camera's cosine curve (0x0050F160). Until the actor's model has loaded, the camera waits, as the world's
|
||||
does (0x0050E900).
|
||||
|
||||
The eye is placed the way the world's follow camera places it (the boom sweep of its solver 0x0050E570):
|
||||
a trace of the camera's faces from the pivot to the eye (0x0050E816) shortens the distance, which then
|
||||
backs off 1/9 yard (0x0050E871 to 0x0050E8A4). With no actor to follow, it is a plain OrbitCamera.
|
||||
|
||||
Create it beside the scene's own camera and give it that camera's settings:
|
||||
local _, cameraIDs = C_ModelInfo.GetModelSceneInfoByID(modelSceneID);
|
||||
local camera = scene:AddCamera(CameraRegistry:CreateCameraByType("FollowCamera"));
|
||||
camera:ApplyFromModelSceneCameraInfo(C_ModelInfo.GetModelSceneCameraInfoByID(cameraIDs[1]), CAMERA_TRANSITION_TYPE_IMMEDIATE, CAMERA_MODIFICATION_TYPE_DISCARD);
|
||||
camera:SetFollowActor(actor);
|
||||
scene:SetActiveCamera(camera);
|
||||
]]
|
||||
|
||||
FollowCameraMixin = CreateFromMixins(OrbitCameraMixin);
|
||||
|
||||
local CAMERA_NAME = "FollowCamera";
|
||||
CameraRegistry:AddCameraFactoryFromMixin(CAMERA_NAME, FollowCameraMixin);
|
||||
|
||||
-- The solver skips the sweep when the distance is not past the near clip by
|
||||
-- more than this (0x008026BC, 2^-20).
|
||||
local SWEEP_MIN_DISTANCE = 9.5367431640625e-07;
|
||||
-- A distance shortened by more than this (0x008029D4, 2^-22) backs off by
|
||||
-- BOOM_BACKOFF (0x00808A00, 1/9 yard), to no less than 0.
|
||||
local SHORTENED_EPSILON = 2.384185791015625e-07;
|
||||
local BOOM_BACKOFF = 1 / 9;
|
||||
-- A height within this of the armed or the live one arms nothing (0x00801360).
|
||||
local ARM_EPSILON = 0.001;
|
||||
-- The ease runs while the live height is this far or more from its target
|
||||
-- (0x008029D4, 2^-22).
|
||||
local EASE_EPSILON = 2.384185791015625e-07;
|
||||
|
||||
-- "public" functions
|
||||
function FollowCameraMixin:GetCameraType() -- override
|
||||
return CAMERA_NAME;
|
||||
end
|
||||
|
||||
-- The target becomes the actor's pivot. Nil stops following.
|
||||
function FollowCameraMixin:SetFollowActor(actor)
|
||||
self.followActor = actor;
|
||||
end
|
||||
|
||||
function FollowCameraMixin:GetFollowActor()
|
||||
return self.followActor;
|
||||
end
|
||||
|
||||
-- "private" functions
|
||||
|
||||
-- ArmHeight (0x005126B0) with no delay and a factor of 1, and its inner
|
||||
-- BeginHeightTransition (0x00512790).
|
||||
function FollowCameraMixin:ArmPivotHeight(height, now)
|
||||
local live = self.pivotHeight or 0;
|
||||
if self.pivotEasing and math.abs(self.pivotTarget - height) < ARM_EPSILON then
|
||||
return;
|
||||
end
|
||||
if math.abs(live - height) < ARM_EPSILON then
|
||||
return;
|
||||
end
|
||||
self.pivotTarget = height;
|
||||
if not self.pivotSnapped then
|
||||
self.pivotSnapped = true;
|
||||
self.pivotHeight = height;
|
||||
self.pivotEasing = false;
|
||||
return;
|
||||
end
|
||||
self.pivotFrom = live;
|
||||
self.pivotStart = now;
|
||||
self.pivotDuration = math.abs(height - live) / tonumber(GetCVar("cameraHeightSmoothSpeed"));
|
||||
self.pivotEasing = true;
|
||||
end
|
||||
|
||||
-- The height channel of the camera's ease (0x0050F160).
|
||||
function FollowCameraMixin:EasePivotHeight(now)
|
||||
if math.abs(self.pivotTarget - self.pivotHeight) < EASE_EPSILON then
|
||||
self.pivotEasing = false;
|
||||
return;
|
||||
end
|
||||
if not self.pivotEasing or self.pivotStart > now then
|
||||
return;
|
||||
end
|
||||
local t = (now - self.pivotStart) / self.pivotDuration;
|
||||
if t >= 1 then
|
||||
self.pivotHeight = self.pivotTarget;
|
||||
else
|
||||
self.pivotHeight = self.pivotFrom + (self.pivotTarget - self.pivotFrom) * (1 - math.cos(math.pi * t)) * 0.5;
|
||||
end
|
||||
end
|
||||
|
||||
function FollowCameraMixin:OnUpdate(elapsed) -- override
|
||||
local actor = self.followActor;
|
||||
if actor then
|
||||
local height = actor:GetCameraPivotHeight(self:GetZoomDistance());
|
||||
if not height then
|
||||
return;
|
||||
end
|
||||
local now = GetTime();
|
||||
self:ArmPivotHeight(height, now);
|
||||
self:EasePivotHeight(now);
|
||||
local x, y, z = actor:GetPosition();
|
||||
self:SetTarget(x, y, z + self.pivotHeight);
|
||||
end
|
||||
OrbitCameraMixin.OnUpdate(self, elapsed);
|
||||
end
|
||||
|
||||
function FollowCameraMixin:CalculatePositionByDistanceFromTarget(targetX, targetY, targetZ, zoomDistance, axisAngleX, axisAngleY, axisAngleZ) -- override
|
||||
if not self.followActor then
|
||||
return OrbitCameraMixin.CalculatePositionByDistanceFromTarget(self, targetX, targetY, targetZ, zoomDistance, axisAngleX, axisAngleY, axisAngleZ);
|
||||
end
|
||||
|
||||
local scene = self:GetOwningScene();
|
||||
local distance = zoomDistance;
|
||||
if distance - scene:GetCameraNearClip() > SWEEP_MIN_DISTANCE then
|
||||
local eyeX, eyeY, eyeZ = OrbitCameraMixin.CalculatePositionByDistanceFromTarget(self, targetX, targetY, targetZ, distance, axisAngleX, axisAngleY, axisAngleZ);
|
||||
local _, _, _, _, _, _, fraction = scene:TraceSegment(targetX, targetY, targetZ, eyeX, eyeY, eyeZ, true);
|
||||
if fraction then
|
||||
distance = distance * fraction;
|
||||
end
|
||||
if math.abs(zoomDistance - distance) > SHORTENED_EPSILON then
|
||||
distance = math.max(distance - BOOM_BACKOFF, 0);
|
||||
end
|
||||
end
|
||||
return OrbitCameraMixin.CalculatePositionByDistanceFromTarget(self, targetX, targetY, targetZ, distance, axisAngleX, axisAngleY, axisAngleZ);
|
||||
end
|
||||
@@ -0,0 +1,144 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/ModelSceneActorMixin.lua.
|
||||
|
||||
|
||||
ModelSceneActorMixin = {};
|
||||
|
||||
-- "public" methods
|
||||
function ModelSceneActorMixin:ApplyFromModelSceneActorInfo(actorInfo)
|
||||
self:SetUseCenterForOrigin(actorInfo.useCenterForOriginX, actorInfo.useCenterForOriginY, actorInfo.useCenterForOriginZ);
|
||||
|
||||
self:SetPosition(actorInfo.position:GetXYZ());
|
||||
self:SetYaw(actorInfo.yaw);
|
||||
self:SetPitch(actorInfo.pitch);
|
||||
self:SetRoll(actorInfo.roll);
|
||||
|
||||
self.requestedScale = nil;
|
||||
|
||||
local actorDisplayInfo = actorInfo.modelActorDisplayID and C_ModelInfo.GetModelSceneActorDisplayInfoByID(actorInfo.modelActorDisplayID);
|
||||
if actorDisplayInfo then
|
||||
if actorDisplayInfo.animationKitID then
|
||||
self:PlayAnimationKit(actorDisplayInfo.animationKitID);
|
||||
else
|
||||
self:SetAnimation(actorDisplayInfo.animation, actorDisplayInfo.animationVariation, actorDisplayInfo.animSpeed);
|
||||
end
|
||||
|
||||
self:SetSpellVisualKit(actorDisplayInfo.spellVisualKitID);
|
||||
|
||||
self:SetAlpha(actorDisplayInfo.alpha);
|
||||
self:SetRequestedScale(actorDisplayInfo.scale);
|
||||
else
|
||||
self:StopAnimationKit();
|
||||
self:SetSpellVisualKit(nil);
|
||||
self:SetAnimation(0, 0, 1.0);
|
||||
self:SetAlpha(1.0);
|
||||
self:SetRequestedScale(1.0);
|
||||
end
|
||||
|
||||
self:SetNormalizedScaleAggressiveness(actorInfo.normalizeScaleAggressiveness or 0.0);
|
||||
|
||||
self:MarkScaleDirty();
|
||||
self:UpdateScale();
|
||||
|
||||
C_ModelInfo.AddActiveModelSceneActor(self, actorInfo.modelActorID);
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:OnModelCleared()
|
||||
C_ModelInfo.ClearActiveModelSceneActor(self);
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:SetOnSizeChangedCallback(onSizeChangedCallback)
|
||||
self.onSizeChangedCallback = onSizeChangedCallback;
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:GetOnSizeChangedCallback()
|
||||
return self.onSizeChangedCallback;
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:SetOnModelLoadedCallback(onModelLoadedCallback)
|
||||
self.onModelLoadedCallback = onModelLoadedCallback;
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:GetOnModelLoadedCallback()
|
||||
return self.onModelLoadedCallback;
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:SetNormalizedScaleAggressiveness(normalizedScaleAggressiveness)
|
||||
if self.normalizedScaleAggressiveness ~= normalizedScaleAggressiveness then
|
||||
self.normalizedScaleAggressiveness = normalizedScaleAggressiveness;
|
||||
self:MarkScaleDirty();
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:GetNormalizedScaleAggressiveness()
|
||||
return self.normalizedScaleAggressiveness or 0;
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:SetRequestedScale(requestedScale)
|
||||
if self.requestedScale ~= requestedScale then
|
||||
self.requestedScale = requestedScale;
|
||||
self:MarkScaleDirty();
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:GetRequestedScale()
|
||||
return self.requestedScale or self:GetScale();
|
||||
end
|
||||
|
||||
MODEL_SCENE_ACTOR_DIMENSIONS_FOR_NORMALIZATION = { width = .9984, depth = 1.121, height = 2.118 }; -- Roughly the dimensions of a human male model (but why male models?)
|
||||
function ModelSceneActorMixin:CalculateNormalizedScale(normalizedScaleAggressiveness)
|
||||
if self:IsLoaded() and normalizedScaleAggressiveness > 0.0 then
|
||||
local bottomX, bottomY, bottomZ, topX, topY, topZ = self:GetActiveBoundingBox(); -- Could be nil for invisible models
|
||||
if bottomX and bottomY and bottomZ and topX and topY and topZ then
|
||||
local width = topX - bottomX;
|
||||
local depth = topY - bottomY;
|
||||
local height = topZ - bottomZ;
|
||||
|
||||
local widthScale = width / MODEL_SCENE_ACTOR_DIMENSIONS_FOR_NORMALIZATION.width;
|
||||
local depthScale = depth / MODEL_SCENE_ACTOR_DIMENSIONS_FOR_NORMALIZATION.depth;
|
||||
local heightScale = height / MODEL_SCENE_ACTOR_DIMENSIONS_FOR_NORMALIZATION.height;
|
||||
|
||||
local maxScale = math.max(widthScale, math.max(depthScale, heightScale));
|
||||
if maxScale == 0 then
|
||||
return 1.0;
|
||||
end
|
||||
|
||||
return 1 / Lerp(1.0, maxScale, normalizedScaleAggressiveness);
|
||||
end
|
||||
end
|
||||
|
||||
return 1.0;
|
||||
end
|
||||
|
||||
-- "private" methods
|
||||
function ModelSceneActorMixin:OnReleased()
|
||||
self:SetOnSizeChangedCallback(nil);
|
||||
self:SetOnModelLoadedCallback(nil);
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:OnUpdate()
|
||||
self:UpdateScale();
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:OnModelLoaded()
|
||||
self:MarkScaleDirty();
|
||||
|
||||
if self.onModelLoadedCallback then
|
||||
self.onModelLoadedCallback(self);
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:UpdateScale()
|
||||
if self.scaleDirty and self:IsLoaded() then
|
||||
self.scaleDirty = nil;
|
||||
local effectiveScale = self:GetRequestedScale() * self:CalculateNormalizedScale(self:GetNormalizedScaleAggressiveness());
|
||||
self:SetScale(effectiveScale);
|
||||
|
||||
if self.onSizeChangedCallback then
|
||||
self.onSizeChangedCallback(self);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:MarkScaleDirty()
|
||||
self.scaleDirty = true;
|
||||
end
|
||||
@@ -0,0 +1,10 @@
|
||||
<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/
|
||||
UI.xsd">
|
||||
<Actor name="ModelSceneActorTemplate" mixin="ModelSceneActorMixin" virtual="true">
|
||||
<Scripts>
|
||||
<OnModelCleared method="OnModelCleared" />
|
||||
<OnModelLoaded method="OnModelLoaded" />
|
||||
<OnUpdate method="OnUpdate" />
|
||||
</Scripts>
|
||||
</Actor>
|
||||
</Ui>
|
||||
@@ -0,0 +1,282 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/ModelSceneControlFrame.lua.
|
||||
|
||||
|
||||
local DEFAULT_ZOOM_INCREMENT = 1;
|
||||
local DEFAULT_ROTATE_INCREMENT = 0.05;
|
||||
|
||||
--------------------------------------------------
|
||||
-- MODEL SCENE CONTROL FRAME MIXIN
|
||||
ModelSceneControlFrameMixin = {};
|
||||
function ModelSceneControlFrameMixin:OnLoad()
|
||||
if self.enableZoom then
|
||||
local increment = self:GetZoomIncrement();
|
||||
self.zoomInButton:SetZoomAmount(increment);
|
||||
self.zoomInButton:Init();
|
||||
|
||||
self.zoomOutButton:SetZoomAmount(-increment);
|
||||
self.zoomOutButton:Init();
|
||||
end
|
||||
self.zoomInButton:SetShown(self.enableZoom);
|
||||
self.zoomOutButton:SetShown(self.enableZoom);
|
||||
|
||||
if self.enableRotate then
|
||||
local increment = self:GetRotateIncrement();
|
||||
self.rotateLeftButton:SetRotation("left");
|
||||
self.rotateLeftButton:SetRotationIncrement(increment);
|
||||
self.rotateLeftButton:Init();
|
||||
|
||||
self.rotateRightButton:SetRotation("right");
|
||||
self.rotateRightButton:SetRotationIncrement(increment);
|
||||
self.rotateRightButton:Init();
|
||||
end
|
||||
self.rotateLeftButton:SetShown(self.enableRotate);
|
||||
self.rotateRightButton:SetShown(self.enableRotate);
|
||||
|
||||
if self.enableReset then
|
||||
self.resetButton:Init();
|
||||
end
|
||||
self.resetButton:SetShown(self.enableReset);
|
||||
end
|
||||
|
||||
function ModelSceneControlFrameMixin:OnShow()
|
||||
self:UpdateLayout();
|
||||
end
|
||||
|
||||
function ModelSceneControlFrameMixin:OnEnter()
|
||||
self:SetAlpha(1);
|
||||
end
|
||||
|
||||
function ModelSceneControlFrameMixin:OnLeave()
|
||||
self:SetAlpha(0.5);
|
||||
end
|
||||
|
||||
function ModelSceneControlFrameMixin:SetZoomIncrement(increment)
|
||||
self.zoomIncrement = increment or DEFAULT_ZOOM_INCREMENT;
|
||||
end
|
||||
|
||||
function ModelSceneControlFrameMixin:GetZoomIncrement()
|
||||
return self.zoomIncrement or DEFAULT_ZOOM_INCREMENT;
|
||||
end
|
||||
|
||||
function ModelSceneControlFrameMixin:SetRotateIncrement(increment)
|
||||
self.rotationIncrement = increment or DEFAULT_ROTATE_INCREMENT;
|
||||
end
|
||||
|
||||
function ModelSceneControlFrameMixin:GetRotateIncrement()
|
||||
return self.rotationIncrement or DEFAULT_ROTATE_INCREMENT;
|
||||
end
|
||||
|
||||
function ModelSceneControlFrameMixin:SetModelScene(modelScene)
|
||||
self.modelScene = modelScene;
|
||||
|
||||
self.zoomInButton:SetModelScene(modelScene);
|
||||
self.zoomOutButton:SetModelScene(modelScene);
|
||||
self.rotateLeftButton:SetModelScene(modelScene);
|
||||
self.rotateRightButton:SetModelScene(modelScene);
|
||||
self.resetButton:SetModelScene(modelScene);
|
||||
end
|
||||
|
||||
function ModelSceneControlFrameMixin:UpdateLayout()
|
||||
local lastButton;
|
||||
local hPadding = self.buttonHorizontalPadding or 0;
|
||||
local vPadding = 0;
|
||||
local buttonSize = 32;
|
||||
local totalHeight = buttonSize;
|
||||
local totalWidth = 2; -- Initial non-zero total width to prevent flickering from negataive padding
|
||||
|
||||
local function LayoutButton(button)
|
||||
button:ClearAllPoints();
|
||||
if lastButton then
|
||||
button:SetPoint("LEFT", lastButton, "RIGHT", hPadding, vPadding);
|
||||
else
|
||||
button:SetPoint("LEFT", 0, vPadding);
|
||||
end
|
||||
totalWidth = totalWidth + buttonSize + hPadding;
|
||||
lastButton = button;
|
||||
end
|
||||
|
||||
if self.enableZoom then
|
||||
-- Make sure that zoom is supported on the model scene.
|
||||
if self.modelScene then
|
||||
local camera = self.modelScene:GetActiveCamera();
|
||||
self.canZoom = camera ~= nil and camera:GetZoomAvailable();
|
||||
self.zoomInButton:SetShown(self.canZoom);
|
||||
self.zoomOutButton:SetShown(self.canZoom);
|
||||
end
|
||||
|
||||
if self.canZoom then
|
||||
LayoutButton(self.zoomInButton);
|
||||
LayoutButton(self.zoomOutButton);
|
||||
end
|
||||
end
|
||||
|
||||
if self.enableRotate then
|
||||
LayoutButton(self.rotateLeftButton);
|
||||
LayoutButton(self.rotateRightButton);
|
||||
end
|
||||
|
||||
if self.enableReset then
|
||||
LayoutButton(self.resetButton);
|
||||
end
|
||||
self:SetSize(totalWidth, totalHeight);
|
||||
end
|
||||
|
||||
--------------------------------------------------
|
||||
-- MODEL SCENE CONTROL BUTTON MIXIN
|
||||
ModelSceneControlButtonMixin = {};
|
||||
function ModelSceneControlButtonMixin:Init(clickTypes, atlas, tooltip, tooltipText)
|
||||
self:RegisterForClicks(clickTypes);
|
||||
if atlas then
|
||||
self.Icon:SetAtlas(atlas);
|
||||
end
|
||||
self.tooltip = tooltip;
|
||||
self.tooltipText = tooltipText;
|
||||
end
|
||||
|
||||
function ModelSceneControlButtonMixin:SetModelScene(modelScene)
|
||||
self.modelScene = modelScene;
|
||||
end
|
||||
|
||||
function ModelSceneControlButtonMixin:OnMouseDown()
|
||||
self.Icon:AdjustPointsOffset(1, -1);
|
||||
self:GetParent().buttonDown = self;
|
||||
end
|
||||
|
||||
function ModelSceneControlButtonMixin:OnMouseUp()
|
||||
self.Icon:AdjustPointsOffset(-1, 1);
|
||||
self:GetParent().buttonDown = nil;
|
||||
end
|
||||
|
||||
function ModelSceneControlButtonMixin:OnClick()
|
||||
--override for your mouse click
|
||||
end
|
||||
|
||||
function ModelSceneControlButtonMixin:OnEnter()
|
||||
self:GetParent():SetAlpha(1);
|
||||
if ( GetCVar("UberTooltips") == "1" ) then
|
||||
local tooltip = GetAppropriateTooltip();
|
||||
local topLevelParent = GetAppropriateTopLevelParent();
|
||||
GameTooltip_SetDefaultAnchor(tooltip, topLevelParent);
|
||||
GameTooltip_SetTitle(tooltip, self.tooltip);
|
||||
if ( self.tooltipText ) then
|
||||
GameTooltip_AddBodyLine(tooltip, self.tooltipText);
|
||||
end
|
||||
tooltip:Show();
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneControlButtonMixin:OnLeave()
|
||||
self:GetParent():SetAlpha(0.5);
|
||||
|
||||
local tooltip = GetAppropriateTooltip();
|
||||
tooltip:Hide();
|
||||
end
|
||||
|
||||
--------------------------------------------------
|
||||
-- MODEL ZOOM BUTTON MIXIN
|
||||
ModelSceneZoomButtonMixin = CreateFromMixins(ModelSceneControlButtonMixin);
|
||||
function ModelSceneZoomButtonMixin:OnLoad()
|
||||
self.zoomAmount = 0;
|
||||
end
|
||||
|
||||
function ModelSceneZoomButtonMixin:Init()
|
||||
if self.zoomAmount < 0 then
|
||||
ModelSceneControlButtonMixin.Init(self, "AnyUp", "common-icon-zoomout", ZOOM_OUT, KEY_MOUSEWHEELDOWN);
|
||||
else
|
||||
ModelSceneControlButtonMixin.Init(self, "AnyUp", "common-icon-zoomin", ZOOM_IN, KEY_MOUSEWHEELUP);
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneZoomButtonMixin:SetZoomAmount(amountToZoom)
|
||||
self.zoomAmount = amountToZoom;
|
||||
end
|
||||
|
||||
function ModelSceneZoomButtonMixin:OnClick()
|
||||
if self.modelScene then
|
||||
self.modelScene:OnMouseWheel(self.zoomAmount);
|
||||
else
|
||||
assertsafe(fale, "Model Scene not specified");
|
||||
end
|
||||
PlaySound(SOUNDKIT.IG_INVENTORY_ROTATE_CHARACTER);
|
||||
end
|
||||
|
||||
--------------------------------------------------
|
||||
-- MODEL SCENE ROTATE BUTTON MIXIN
|
||||
ModelScenelRotateButtonMixin = CreateFromMixins(ModelSceneControlButtonMixin);
|
||||
function ModelScenelRotateButtonMixin:OnLoad()
|
||||
ModelSceneControlButtonMixin.OnLoad(self);
|
||||
self.rotateDirection = "none";
|
||||
end
|
||||
|
||||
function ModelScenelRotateButtonMixin:Init()
|
||||
if (self.rotateDirection == "left") then
|
||||
ModelSceneControlButtonMixin.Init(self, "AnyUp", "common-icon-rotateleft", ROTATE_LEFT, ROTATE_TOOLTIP);
|
||||
elseif (self.rotateDirection == "right") then
|
||||
ModelSceneControlButtonMixin.Init(self, "AnyUp", "common-icon-rotateright", ROTATE_RIGHT, ROTATE_TOOLTIP);
|
||||
else
|
||||
assertsafe(false, "Invalid rotation specified: "..tostring(self.rotateDirection));
|
||||
end
|
||||
end
|
||||
|
||||
function ModelScenelRotateButtonMixin:SetRotation(rotation)
|
||||
self.rotateDirection = rotation;
|
||||
end
|
||||
|
||||
function ModelScenelRotateButtonMixin:SetRotationIncrement(increment)
|
||||
self.rotationIncrement = increment or 0.05;
|
||||
end
|
||||
|
||||
function ModelScenelRotateButtonMixin:OnMouseDown()
|
||||
ModelSceneControlButtonMixin.OnMouseDown(self);
|
||||
if self.modelScene then
|
||||
self.modelScene:AdjustCameraYaw(self.rotateDirection, self.rotationIncrement);
|
||||
else
|
||||
assertsafe(false, "Model Scene not specified");
|
||||
end
|
||||
PlaySound(SOUNDKIT.IG_INVENTORY_ROTATE_CHARACTER);
|
||||
end
|
||||
|
||||
function ModelScenelRotateButtonMixin:OnMouseUp()
|
||||
ModelSceneControlButtonMixin.OnMouseUp(self);
|
||||
if self.modelScene then
|
||||
self.modelScene:StopCameraYaw();
|
||||
end
|
||||
end
|
||||
|
||||
function ModelScenelRotateButtonMixin:OnSmartNavClick()
|
||||
self:RotateStep();
|
||||
end
|
||||
|
||||
function ModelScenelRotateButtonMixin:RotateStep()
|
||||
local camera = self.modelScene and self.modelScene:GetActiveCamera();
|
||||
if not camera then
|
||||
return;
|
||||
end
|
||||
|
||||
if self.rotateDirection == "left" then
|
||||
camera:AdjustYaw(-1, -1, 0.25);
|
||||
else
|
||||
camera:AdjustYaw(1, 1, 0.25);
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------
|
||||
-- MODEL SCENE RESET BUTTON MIXIN
|
||||
ModelSceneResetButtonMixin = CreateFromMixins(ModelSceneControlButtonMixin);
|
||||
function ModelSceneResetButtonMixin:OnLoad()
|
||||
ModelSceneControlButtonMixin.OnLoad(self);
|
||||
end
|
||||
|
||||
function ModelSceneResetButtonMixin:Init()
|
||||
local tooltipText = nil;
|
||||
ModelSceneControlButtonMixin.Init(self, "AnyUp", "common-icon-undo", RESET_POSITION, tooltipText);
|
||||
end
|
||||
|
||||
function ModelSceneResetButtonMixin:OnClick()
|
||||
if self.modelScene then
|
||||
self.modelScene:Reset();
|
||||
else
|
||||
assertsafe(false, "Model Scene not specified");
|
||||
end
|
||||
PlaySound(SOUNDKIT.IG_INVENTORY_ROTATE_CHARACTER);
|
||||
end
|
||||
@@ -0,0 +1,67 @@
|
||||
<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/
|
||||
UI.xsd">
|
||||
<!-- Retail's Blizzard_SharedXML/ModelSceneControlFrame.xml. The one change:
|
||||
HitRectInsets takes this client's <AbsInset> child. The atlases are
|
||||
built in (src/texture/AtlasData.h, EMBEDDED ART). -->
|
||||
<Button name="ModelSceneControlButtonTemplate" mixin="ModelSceneControlButtonMixin" virtual="true">
|
||||
<Size x="32" y="32"/>
|
||||
<HitRectInsets>
|
||||
<AbsInset left="4" right="4" top="4" bottom="4"/>
|
||||
</HitRectInsets>
|
||||
<Layers>
|
||||
<Layer level="OVERLAY">
|
||||
<Texture parentKey="Icon" >
|
||||
<Size x="16" y="16"/>
|
||||
<Anchors>
|
||||
<Anchor point="CENTER"/>
|
||||
</Anchors>
|
||||
</Texture>
|
||||
<!--<Texture parentKey="Icon" setAllPoints="true" />-->
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnMouseDown method="OnMouseDown" />
|
||||
<OnMouseUp method="OnMouseUp" />
|
||||
<OnClick method="OnClick" />
|
||||
<OnEnter method="OnEnter" />
|
||||
<OnLeave method="OnLeave" />
|
||||
</Scripts>
|
||||
<NormalTexture parentKey="NormalTexture" atlas="common-button-square-gray-up"/>
|
||||
<HighlightTexture parentKey="HighlightTexture" alphaMode="ADD" alpha="0.4">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeKey="$parent.Icon" relativePoint="TOPLEFT"/>
|
||||
<Anchor point="BOTTOMRIGHT" relativeKey="$parent.Icon" relativePoint="BOTTOMRIGHT"/>
|
||||
</Anchors>
|
||||
</HighlightTexture>
|
||||
<PushedTexture parentKey="PushedTexture" atlas="common-button-square-gray-down">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" x="1" y="-1"/>
|
||||
<Anchor point="BOTTOMRIGHT" x="1" y="-1"/>
|
||||
</Anchors>
|
||||
</PushedTexture>
|
||||
</Button>
|
||||
<Frame name="ModelSceneControlFrameTemplate" virtual="true" mixin="ModelSceneControlFrameMixin" alpha="0.5" hidden="true">
|
||||
<Size x="32" y="32"/>
|
||||
<KeyValues>
|
||||
<KeyValue key="enableZoom" value="true" type="boolean" />
|
||||
<KeyValue key="enableRotate" value="true" type="boolean" />
|
||||
<KeyValue key="enableReset" value="true" type="boolean" />
|
||||
<KeyValue key="rotationIncrement" value="0.05" type="number" />
|
||||
<KeyValue key="zoomIncrement" value="1" type="number" />
|
||||
<KeyValue key="buttonHorizontalPadding" value="-6" type="number" />
|
||||
</KeyValues>
|
||||
<Frames>
|
||||
<Button parentKey="zoomInButton" hidden="true" mixin="ModelSceneZoomButtonMixin" inherits="ModelSceneControlButtonTemplate"/>
|
||||
<Button parentKey="zoomOutButton" hidden="true" mixin="ModelSceneZoomButtonMixin" inherits="ModelSceneControlButtonTemplate"/>
|
||||
<Button parentKey="rotateLeftButton" hidden="true" mixin="ModelScenelRotateButtonMixin" inherits="ModelSceneControlButtonTemplate"/>
|
||||
<Button parentKey="rotateRightButton" hidden="true" mixin="ModelScenelRotateButtonMixin" inherits="ModelSceneControlButtonTemplate"/>
|
||||
<Button parentKey="resetButton" hidden="true" mixin="ModelSceneResetButtonMixin" inherits="ModelSceneControlButtonTemplate"/>
|
||||
</Frames>
|
||||
<Scripts>
|
||||
<OnLoad method="OnLoad" />
|
||||
<OnShow method="OnShow" />
|
||||
<OnEnter method="OnEnter" />
|
||||
<OnLeave method="OnLeave" />
|
||||
</Scripts>
|
||||
</Frame>
|
||||
</Ui>
|
||||
@@ -0,0 +1,571 @@
|
||||
-- Backport of retail's Blizzard_SharedXML/ModelSceneMixin.lua, verbatim except where
|
||||
-- marked "ClassicAPI:".
|
||||
|
||||
|
||||
ModelSceneMixin = {}
|
||||
|
||||
-- "public" functions
|
||||
function ModelSceneMixin:OnLoad()
|
||||
self.cameras = {};
|
||||
self.actorTemplate = "ModelSceneActorTemplate";
|
||||
self.tagToActor = {};
|
||||
self.tagToCamera = {};
|
||||
|
||||
if self.allowOverlappedModels then
|
||||
self:SetAllowOverlappedModels(true);
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneMixin:ClearScene()
|
||||
self.modelSceneID = nil;
|
||||
|
||||
self:ReleaseAllActors();
|
||||
self:ReleaseAllCameras();
|
||||
|
||||
C_ModelInfo.ClearActiveModelScene(self);
|
||||
end
|
||||
|
||||
function ModelSceneMixin:GetModelSceneID()
|
||||
return self.modelSceneID;
|
||||
end
|
||||
|
||||
-- Adjusts this scene to mirror a model scene from static data without transition
|
||||
function ModelSceneMixin:SetFromModelSceneID(modelSceneID, forceEvenIfSame, noAutoCreateActors)
|
||||
local modelSceneType, cameraIDs, actorIDs = C_ModelInfo.GetModelSceneInfoByID(modelSceneID);
|
||||
if not modelSceneType then
|
||||
return;
|
||||
end
|
||||
|
||||
if self.modelSceneID ~= modelSceneID or forceEvenIfSame then
|
||||
self.modelSceneID = modelSceneID;
|
||||
self:ReleaseAllActors();
|
||||
self:ReleaseAllCameras();
|
||||
|
||||
if not noAutoCreateActors then
|
||||
for actorIndex, actorID in ipairs(actorIDs) do
|
||||
self:CreateActorFromScene(actorID);
|
||||
end
|
||||
end
|
||||
|
||||
for cameraIndex, cameraID in ipairs(cameraIDs) do
|
||||
self:CreateCameraFromScene(cameraID);
|
||||
end
|
||||
end
|
||||
|
||||
C_ModelInfo.AddActiveModelScene(self, self.modelSceneID);
|
||||
EventRegistry:TriggerEvent("ModelScene.SetFromModelSceneID", self, self.modelSceneID);
|
||||
end
|
||||
|
||||
function ModelSceneMixin:SetResetCallback(callback)
|
||||
self.resetCallback = callback;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:Reset()
|
||||
if self.modelSceneID then
|
||||
self:TransitionToModelSceneID(self.modelSceneID, self.cameraTransitionType, self.cameraModificationType, self.forceEvenIfSame);
|
||||
if self.resetCallback then
|
||||
self.resetCallback(self);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Adjusts this scene to mirror a model scene from static data but with transition effects
|
||||
-- Only actors/cameras with script tags will be transitioned
|
||||
function ModelSceneMixin:TransitionToModelSceneID(modelSceneID, cameraTransitionType, cameraModificationType, forceEvenIfSame)
|
||||
local modelSceneType, cameraIDs, actorIDs = C_ModelInfo.GetModelSceneInfoByID(modelSceneID);
|
||||
if not modelSceneType or #cameraIDs == 0 or #actorIDs == 0 then
|
||||
return;
|
||||
end
|
||||
|
||||
if self.modelSceneID ~= modelSceneID or forceEvenIfSame then
|
||||
self.modelSceneID = modelSceneID;
|
||||
self.cameraTransitionType = cameraTransitionType;
|
||||
self.cameraModificationType = cameraModificationType;
|
||||
self.forceEvenIfSame = forceEvenIfSame;
|
||||
|
||||
local actorsToRelease = {};
|
||||
for actor in self:EnumerateActiveActors() do
|
||||
actorsToRelease[actor] = true;
|
||||
end
|
||||
|
||||
local oldTagToActor = self.tagToActor;
|
||||
self.tagToActor = {};
|
||||
|
||||
for actorIndex, actorID in ipairs(actorIDs) do
|
||||
local actor = self:CreateOrTransitionActorFromScene(oldTagToActor, actorID);
|
||||
if actor then
|
||||
actorsToRelease[actor] = nil;
|
||||
end
|
||||
end
|
||||
|
||||
for actor in pairs(actorsToRelease) do
|
||||
self.actorPool:Release(actor);
|
||||
end
|
||||
|
||||
local oldTagToCamera = self.tagToCamera;
|
||||
self.tagToCamera = {};
|
||||
|
||||
self.cameras = {};
|
||||
|
||||
local needsNewCamera = true;
|
||||
for cameraIndex, cameraID in ipairs(cameraIDs) do
|
||||
local camera = self:CreateOrTransitionCameraFromScene(oldTagToCamera, cameraTransitionType, cameraModificationType, cameraID);
|
||||
if camera == self.activeCamera then
|
||||
needsNewCamera = false;
|
||||
end
|
||||
end
|
||||
|
||||
if needsNewCamera then
|
||||
self:SetActiveCamera(self.cameras[1]);
|
||||
end
|
||||
end
|
||||
|
||||
C_ModelInfo.AddActiveModelScene(self, self.modelSceneID);
|
||||
EventRegistry:TriggerEvent("ModelScene.TransitionToModelSceneID", self, self.modelSceneID);
|
||||
end
|
||||
|
||||
-- There may be inactive (pooled) actors maintained by this scene, these function only returns the active actors
|
||||
function ModelSceneMixin:GetNumActiveActors()
|
||||
return self.actorPool and self.actorPool:GetNumActive() or 0;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:EnumerateActiveActors()
|
||||
if self.actorPool then
|
||||
return self.actorPool:EnumerateActive();
|
||||
end
|
||||
return nop;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:GetActorByTag(tag)
|
||||
return self.tagToActor[tag];
|
||||
end
|
||||
|
||||
function ModelSceneMixin:AttachPlayerToMount(mountActor, animID, isSelfMount, disablePlayerMountPreview, spellVisualKitID, usePlayerNativeForm, optionalPlayerRiderTag)
|
||||
local playerRiderTag = optionalPlayerRiderTag or "player-rider";
|
||||
local playerActor = self:GetPlayerActor(playerRiderTag);
|
||||
if (playerActor) then
|
||||
if disablePlayerMountPreview or isSelfMount then
|
||||
playerActor:ClearModel();
|
||||
else
|
||||
local sheatheWeapons = true;
|
||||
local autoDress = true;
|
||||
local hideWeapons = true;
|
||||
|
||||
if C_Glue.IsOnGlueScreen() then
|
||||
local characterIndex = nil; -- defaults to selected character.
|
||||
playerActor:SetPlayerModelFromGlues(characterIndex, sheatheWeapons, autoDress, hideWeapons, usePlayerNativeForm);
|
||||
else
|
||||
playerActor:SetModelByUnit("player", sheatheWeapons, autoDress, hideWeapons, usePlayerNativeForm);
|
||||
end
|
||||
local calcMountScale = mountActor:CalculateMountScale(playerActor);
|
||||
local inverseScale = 1 / calcMountScale;
|
||||
playerActor:SetRequestedScale( inverseScale );
|
||||
mountActor:AttachToMount(playerActor, animID, spellVisualKitID);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function GetPlayerActorLabelTag(forceAlternateForm, overrideRaceFilename, overrideGender)
|
||||
local playerRaceName = overrideRaceFilename;
|
||||
local playerGender = overrideGender;
|
||||
local playerRaceNameActorTag = nil;
|
||||
local hasAlternateForm, inAlternateForm = false, false;
|
||||
if not overrideRaceFilename or not overrideGender then
|
||||
if C_Glue.IsOnGlueScreen() then
|
||||
local characterGuid = GetCharacterGUID(GetCharacterSelection());
|
||||
if characterGuid then
|
||||
local basicCharacterInfo = GetBasicCharacterInfo(characterGuid);
|
||||
playerRaceName = overrideRaceFilename or basicCharacterInfo.raceFilename;
|
||||
playerGender = overrideGender or basicCharacterInfo.genderEnum;
|
||||
end
|
||||
else
|
||||
hasAlternateForm, inAlternateForm = C_PlayerInfo.GetAlternateFormInfo();
|
||||
local _, raceFilename = UnitRace("player");
|
||||
playerRaceName = overrideRaceFilename or raceFilename;
|
||||
playerGender = overrideGender or UnitSex("player");
|
||||
end
|
||||
end
|
||||
if not playerRaceName or not playerGender then
|
||||
return playerRaceName, playerRaceNameActorTag;
|
||||
end
|
||||
playerRaceName = playerRaceName:lower();
|
||||
if hasAlternateForm and (inAlternateForm or forceAlternateForm) then
|
||||
playerRaceName = playerRaceName.."-alt";
|
||||
end
|
||||
playerGender = (playerGender == 2) and "male" or "female";
|
||||
playerRaceNameActorTag = playerRaceName.."-"..playerGender;
|
||||
return playerRaceName, playerRaceNameActorTag;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:GetPlayerActor(overrideActorName, forceAlternateForm, overrideRaceFilename, overrideGender)
|
||||
local actor;
|
||||
if overrideActorName then
|
||||
actor = self:GetActorByTag(overrideActorName);
|
||||
else
|
||||
local playerRaceName, playerRaceNameActorTag = GetPlayerActorLabelTag(forceAlternateForm, overrideRaceFilename, overrideGender);
|
||||
actor = self:GetActorByTag(playerRaceNameActorTag);
|
||||
if not actor then
|
||||
actor = self:GetActorByTag(playerRaceName);
|
||||
if not actor then
|
||||
actor = self:GetActorByTag("player");
|
||||
if not actor then
|
||||
actor = self:GetActorByTag("player-rider");
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return actor;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:ReleaseAllActors()
|
||||
if self.actorPool then
|
||||
self.actorPool:ReleaseAll();
|
||||
|
||||
if self.dropShadowPool then
|
||||
self.dropShadowPool:ReleaseAll();
|
||||
end
|
||||
|
||||
self.tagToActor = {};
|
||||
end
|
||||
end
|
||||
|
||||
local function OnReleased(actorPool, actor)
|
||||
actor:OnReleased();
|
||||
ActorPool_HideAndClearModel(actorPool, actor);
|
||||
end
|
||||
|
||||
function ModelSceneMixin:AcquireActor()
|
||||
if not self.actorPool then
|
||||
self.actorPool = CreateActorPool(self, self.actorTemplate, OnReleased);
|
||||
end
|
||||
return self.actorPool:Acquire();
|
||||
end
|
||||
|
||||
function ModelSceneMixin:ReleaseActor(actor)
|
||||
if not actor or not self.actorPool then
|
||||
return;
|
||||
end
|
||||
|
||||
for tag, taggedActor in pairs(self.tagToActor) do
|
||||
if actor == taggedActor then
|
||||
self.tagToActor[tag] = nil;
|
||||
break;
|
||||
end
|
||||
end
|
||||
|
||||
if actor.dropShadowTexture then
|
||||
self.dropShadowPool:Release(actor.dropShadowTexture);
|
||||
end
|
||||
|
||||
return self.actorPool:Release(actor);
|
||||
end
|
||||
|
||||
function ModelSceneMixin:AcquireAndInitializeActor(actorInfo)
|
||||
local actor = self:AcquireActor();
|
||||
self:InitializeActor(actor, actorInfo);
|
||||
return actor;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:ReleaseAllCameras()
|
||||
self:SetActiveCamera(nil);
|
||||
for i = #self.cameras, 1, -1 do
|
||||
self.cameras[i]:SetOwningScene(nil);
|
||||
self.cameras[i] = nil;
|
||||
end
|
||||
self.tagToCamera = {};
|
||||
end
|
||||
|
||||
function ModelSceneMixin:GetCameraByTag(tag)
|
||||
return self.tagToCamera[tag];
|
||||
end
|
||||
|
||||
function ModelSceneMixin:AddCamera(camera)
|
||||
table.insert(self.cameras, camera);
|
||||
|
||||
camera:SetOwningScene(self);
|
||||
|
||||
if not self:HasActiveCamera() then
|
||||
self:SetActiveCamera(camera);
|
||||
end
|
||||
|
||||
return camera;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:HasActiveCamera()
|
||||
return self.activeCamera ~= nil;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:GetActiveCamera()
|
||||
return self.activeCamera;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:SetActiveCamera(camera)
|
||||
if camera ~= self.activeCamera then
|
||||
if self.activeCamera then
|
||||
self.activeCamera:OnDeactivated();
|
||||
end
|
||||
|
||||
self.activeCamera = camera;
|
||||
|
||||
if self.activeCamera then
|
||||
self.activeCamera:OnActivated();
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneMixin:IsLeftMouseButtonDown()
|
||||
return self.isLeftButtonDown;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:IsRightMouseButtonDown()
|
||||
return self.isRightButtonDown;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:Transform3DPointTo2D(x, y, z)
|
||||
self:SynchronizeActiveCamera();
|
||||
return self:Project3DPointTo2D(x, y, z);
|
||||
end
|
||||
|
||||
-- "private" functions
|
||||
function ModelSceneMixin:OnUpdate(elapsed)
|
||||
if self.activeCamera then
|
||||
local yawDirection = self.yawDirection;
|
||||
-- ClassicAPI: retail turns by the increment every frame, which spins
|
||||
-- several times faster at this client's uncapped frame rates. The
|
||||
-- increment counts per 1/60 s here: 0.05 is 3 rad/s, the speed of
|
||||
-- 1.12's own rotate buttons (ROTATIONS_PER_SECOND, pi rad/s).
|
||||
local increment = self.increment and self.increment * elapsed * 60;
|
||||
if yawDirection == "left" then
|
||||
self.activeCamera:AdjustYaw(-1, -1, increment);
|
||||
elseif yawDirection == "right" then
|
||||
self.activeCamera:AdjustYaw(1, 1, increment);
|
||||
end
|
||||
|
||||
self.activeCamera:OnUpdate(elapsed);
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneMixin:SynchronizeActiveCamera()
|
||||
if self.activeCamera then
|
||||
self.activeCamera:SynchronizeCamera();
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneMixin:OnEnter(button)
|
||||
if self.ControlFrame then
|
||||
self.ControlFrame:Show();
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneMixin:OnLeave(button)
|
||||
if self.ControlFrame then
|
||||
if not self.ControlFrame:IsMouseOver()then
|
||||
self.ControlFrame:Hide();
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneMixin:OnMouseDown(button)
|
||||
if button == "LeftButton" then
|
||||
self.isLeftButtonDown = true;
|
||||
elseif button == "RightButton" then
|
||||
self.isRightButtonDown = true;
|
||||
end
|
||||
|
||||
if self.activeCamera then
|
||||
self.activeCamera:OnMouseDown(button);
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneMixin:OnMouseUp(button)
|
||||
if button == "LeftButton" then
|
||||
self.isLeftButtonDown = false;
|
||||
elseif button == "RightButton" then
|
||||
self.isRightButtonDown = false;
|
||||
end
|
||||
|
||||
if self.activeCamera then
|
||||
self.activeCamera:OnMouseUp(button);
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneMixin:OnMouseWheel(delta)
|
||||
if self.activeCamera then
|
||||
self.activeCamera:OnMouseWheel(delta);
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneMixin:AdjustCameraYaw(direction, increment)
|
||||
if self.activeCamera then
|
||||
self.yawDirection = direction;
|
||||
self.increment = increment;
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneMixin:StopCameraYaw()
|
||||
self.yawDirection = nil;
|
||||
self.increment = nil;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:InitializeActor(actor, actorInfo)
|
||||
if actorInfo.scriptTag then
|
||||
self.tagToActor[actorInfo.scriptTag] = actor;
|
||||
end
|
||||
|
||||
actor:ApplyFromModelSceneActorInfo(actorInfo);
|
||||
|
||||
actor:Show();
|
||||
end
|
||||
|
||||
function ModelSceneMixin:CreateActorFromScene(actorID)
|
||||
local actorInfo = C_ModelInfo.GetModelSceneActorInfoByID(actorID);
|
||||
return self:AcquireAndInitializeActor(actorInfo);
|
||||
end
|
||||
|
||||
function ModelSceneMixin:CreateOrTransitionActorFromScene(oldTagToActor, actorID)
|
||||
local actorInfo = C_ModelInfo.GetModelSceneActorInfoByID(actorID);
|
||||
local existingActor = oldTagToActor[actorInfo.scriptTag];
|
||||
if existingActor then
|
||||
self:InitializeActor(existingActor, actorInfo);
|
||||
return existingActor;
|
||||
end
|
||||
|
||||
return self:AcquireAndInitializeActor(actorInfo);
|
||||
end
|
||||
|
||||
function ModelSceneMixin:CreateCameraFromScene(modelSceneCameraID)
|
||||
local modelSceneCameraInfo = C_ModelInfo.GetModelSceneCameraInfoByID(modelSceneCameraID);
|
||||
if modelSceneCameraInfo then
|
||||
local camera = CameraRegistry:CreateCameraByType(modelSceneCameraInfo.cameraType);
|
||||
if camera then
|
||||
if modelSceneCameraInfo.scriptTag then
|
||||
self.tagToCamera[modelSceneCameraInfo.scriptTag] = camera;
|
||||
end
|
||||
self:AddCamera(camera);
|
||||
camera:ApplyFromModelSceneCameraInfo(modelSceneCameraInfo, CAMERA_TRANSITION_TYPE_IMMEDIATE, CAMERA_MODIFICATION_TYPE_DISCARD);
|
||||
return camera;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneMixin:CreateOrTransitionCameraFromScene(oldTagToCamera, cameraTransitionType, cameraModificationType, modelSceneCameraID)
|
||||
local modelSceneCameraInfo = C_ModelInfo.GetModelSceneCameraInfoByID(modelSceneCameraID);
|
||||
if modelSceneCameraInfo then
|
||||
local existingCamera = oldTagToCamera[modelSceneCameraInfo.scriptTag];
|
||||
if existingCamera and existingCamera:GetCameraType() == modelSceneCameraInfo.cameraType then
|
||||
self.tagToCamera[modelSceneCameraInfo.scriptTag] = existingCamera;
|
||||
|
||||
self:AddCamera(existingCamera);
|
||||
existingCamera:ApplyFromModelSceneCameraInfo(modelSceneCameraInfo, cameraTransitionType, cameraModificationType);
|
||||
return existingCamera;
|
||||
end
|
||||
|
||||
return self:CreateCameraFromScene(modelSceneCameraID);
|
||||
end
|
||||
end
|
||||
|
||||
-- actorSettings = {
|
||||
-- "actorTag" = { startDelay = 0, duration = 0, speed = 1 } -- duration 0 means no automatic stoppage
|
||||
-- }
|
||||
function ModelSceneMixin:ShowAndAnimateActors(actorSettings, onFinishedCallback)
|
||||
self:Show();
|
||||
local totalTime = 0;
|
||||
for actorTag, actorInfo in pairs(actorSettings) do
|
||||
local actor = self:GetActorByTag(actorTag);
|
||||
if actor then
|
||||
local runningTime = actorInfo.startDelay + actorInfo.duration;
|
||||
if actorInfo.startDelay > 0 then
|
||||
actor:SetAnimation(0, 0, 0, 0);
|
||||
C_Timer.After(actorInfo.startDelay,
|
||||
function()
|
||||
actor:SetAnimation(0, 0, actorInfo.speed, 0);
|
||||
end
|
||||
);
|
||||
else
|
||||
actor:SetAnimation(0, 0, actorInfo.speed, 0);
|
||||
end
|
||||
if actorInfo.duration > 0 then
|
||||
C_Timer.After(runningTime,
|
||||
function()
|
||||
actor:SetAnimation(0, 0, 0, 0);
|
||||
end
|
||||
);
|
||||
end
|
||||
if runningTime > totalTime then
|
||||
totalTime = runningTime;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if onFinishedCallback and totalTime > 0 then
|
||||
C_Timer.After(totalTime, onFinishedCallback);
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneMixin:GetOrAcquireDropShadow(actor)
|
||||
if actor.dropShadowTexture then
|
||||
return actor.dropShadowTexture;
|
||||
end
|
||||
|
||||
if not self.dropShadowPool then
|
||||
self.dropShadowPool = CreateTexturePool(self, "BORDER", 1, "ModelSceneDropShadowTemplate");
|
||||
end
|
||||
|
||||
actor.dropShadowTexture = self.dropShadowPool:Acquire();
|
||||
return actor.dropShadowTexture;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:AddOrUpdateDropShadow(actor, baseShadowScale)
|
||||
baseShadowScale = baseShadowScale or 1.0;
|
||||
|
||||
local dropShadowTexture = self:GetOrAcquireDropShadow(actor);
|
||||
local positionVector = CreateVector3D(actor:GetPosition());
|
||||
positionVector:ScaleBy(actor:GetScale());
|
||||
local x, y, depthScale = self:Transform3DPointTo2D(positionVector:GetXYZ());
|
||||
|
||||
if not x or not y or not depthScale then
|
||||
dropShadowTexture:Hide();
|
||||
return;
|
||||
end
|
||||
|
||||
dropShadowTexture:ClearAllPoints();
|
||||
depthScale = baseShadowScale * Lerp(.05, 1, ClampedPercentageBetween(depthScale, .8, 1))
|
||||
-- Scales down the texture depending on it's depthScale.
|
||||
dropShadowTexture:SetScale(depthScale);
|
||||
|
||||
-- Need to apply the effective scale to account for UI Scaling.
|
||||
local inverseScale = self:GetEffectiveScale() * depthScale;
|
||||
-- The position of the character can be found by the offset on the screen.
|
||||
dropShadowTexture:SetPoint("CENTER", self, "BOTTOMLEFT", (x / inverseScale) + 2, (y / inverseScale) - 4);
|
||||
dropShadowTexture:Show();
|
||||
end
|
||||
|
||||
|
||||
PanningModelSceneMixin = CreateFromMixins(ModelSceneMixin);
|
||||
function PanningModelSceneMixin:TransitionToModelSceneID(modelSceneID, cameraTransitionType, cameraModificationType, forceEvenIfSame)
|
||||
ModelSceneMixin.TransitionToModelSceneID(self, modelSceneID, cameraTransitionType, cameraModificationType, forceEvenIfSame);
|
||||
|
||||
local camera = self:GetActiveCamera();
|
||||
if camera then
|
||||
camera:SetRightMouseButtonXMode(ORBIT_CAMERA_MOUSE_PAN_HORIZONTAL, true);
|
||||
camera:SetRightMouseButtonYMode(ORBIT_CAMERA_MOUSE_PAN_VERTICAL, true);
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
NoCameraControlModelSceneMixin = CreateFromMixins(ModelSceneMixin);
|
||||
function NoCameraControlModelSceneMixin:OnMouseDown(button)
|
||||
self.isLeftButtonDown = false;
|
||||
self.isRightButtonDown = false;
|
||||
end
|
||||
|
||||
function NoCameraControlModelSceneMixin:OnMouseUp(button)
|
||||
self.isLeftButtonDown = false;
|
||||
self.isRightButtonDown = false;
|
||||
end
|
||||
|
||||
function NoCameraControlModelSceneMixin:OnMouseWheel(delta)
|
||||
end
|
||||
NoZoomModelSceneMixin = CreateFromMixins(ModelSceneMixin);
|
||||
function NoZoomModelSceneMixin:OnMouseWheel(delta)
|
||||
end
|
||||
@@ -0,0 +1,28 @@
|
||||
<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/
|
||||
UI.xsd">
|
||||
<ModelScene name="NonInteractableModelSceneMixinTemplate" mixin="ModelSceneMixin" virtual="true">
|
||||
<Scripts>
|
||||
<OnLoad method="OnLoad" />
|
||||
<OnUpdate method="OnUpdate" />
|
||||
</Scripts>
|
||||
</ModelScene>
|
||||
<ModelScene name="ModelSceneMixinTemplate" inherits="NonInteractableModelSceneMixinTemplate" virtual="true">
|
||||
<Scripts>
|
||||
<OnEnter method="OnEnter"/>
|
||||
<OnLeave method="OnLeave"/>
|
||||
<OnMouseUp method="OnMouseUp"/>
|
||||
<OnMouseDown method="OnMouseDown"/>
|
||||
<OnMouseWheel method="OnMouseWheel" />
|
||||
</Scripts>
|
||||
</ModelScene>
|
||||
<ModelScene name="StoreModelSceneMixinTemplate" inherits="ModelSceneMixinTemplate" virtual="true">
|
||||
<KeyValues>
|
||||
<!-- CLASS-44103: Automatically allow overlapped models for shop scenes -->
|
||||
<KeyValue key="allowOverlappedModels" value="true" type="boolean"/>
|
||||
</KeyValues>
|
||||
</ModelScene>
|
||||
<ModelScene name="NoZoomModelSceneMixinTemplate" inherits="ModelSceneMixinTemplate" mixin="NoZoomModelSceneMixin" virtual="true"/>
|
||||
<ModelScene name="PanningModelSceneMixinTemplate" inherits="ModelSceneMixinTemplate" mixin="PanningModelSceneMixin" virtual="true" />
|
||||
<ModelScene name="NoCameraControlModelSceneMixinTemplate" inherits="ModelSceneMixinTemplate" mixin="NoCameraControlModelSceneMixin" virtual="true" />
|
||||
<Texture name="ModelSceneDropShadowTemplate" atlas="scoreboard-charactermodels-shadow" useAtlasSize="true" virtual="true"/>
|
||||
</Ui>
|
||||
@@ -0,0 +1,640 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/Shared/ModelSceneCameras/OrbitCameraMixin.lua.
|
||||
|
||||
--[[
|
||||
OrbitCameraMixin - A simple camera that can orbit a target point at a fixed distance.
|
||||
|
||||
The orbit is represented using a distance from a target point in world space and yaw, pitch and roll.
|
||||
The yaw, pitch and roll represent the camera's orientation, or inversely the direction of the camera from the target point.
|
||||
|
||||
Optionally, the target, orientation and zoom can have splines that are added to the respective axes. The splines are fed the zoom percentage to calculate the point on the spline.
|
||||
This is ideal for changing view points based on zoom distance from the target. For example, target model's face on zoom in and center on the model on zoom out.
|
||||
]]
|
||||
|
||||
OrbitCameraMixin = CreateFromMixins(CameraBaseMixin);
|
||||
|
||||
local CAMERA_NAME = "OrbitCamera";
|
||||
CameraRegistry:AddCameraFactoryFromMixin(CAMERA_NAME, OrbitCameraMixin);
|
||||
|
||||
-- "public" functions
|
||||
function OrbitCameraMixin:GetCameraType() -- override
|
||||
return CAMERA_NAME;
|
||||
end
|
||||
|
||||
local function TryCreateZoomSpline(x, y, z, existingSpline)
|
||||
if x and y and z and (x ~= 0 or y ~= 0 or z ~= 0) then
|
||||
local spline = existingSpline or CreateCatmullRomSpline(3);
|
||||
spline:ClearPoints();
|
||||
spline:AddPoint(0, 0, 0);
|
||||
spline:AddPoint(x, y, z);
|
||||
|
||||
return spline;
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:ApplyFromModelSceneCameraInfo(modelSceneCameraInfo, transitionType, modificationType) -- override
|
||||
self.panningXOffset = 0;
|
||||
self.panningYOffset = 0;
|
||||
|
||||
local transitionalCameraInfo = self:CalculateTransitionalValues(self.modelSceneCameraInfo, modelSceneCameraInfo, modificationType);
|
||||
|
||||
self.modelSceneCameraInfo = modelSceneCameraInfo;
|
||||
|
||||
self:SetTarget(transitionalCameraInfo.target:GetXYZ());
|
||||
self:SetTargetSpline(TryCreateZoomSpline(transitionalCameraInfo.zoomedTargetOffset:GetXYZ()), self:GetTargetSpline());
|
||||
self:SetOrientationSpline(TryCreateZoomSpline(transitionalCameraInfo.zoomedYawOffset, transitionalCameraInfo.zoomedPitchOffset, transitionalCameraInfo.zoomedRollOffset), self:GetOrientationSpline());
|
||||
|
||||
self:SetMinZoomDistance(transitionalCameraInfo.minZoomDistance);
|
||||
self:SetMaxZoomDistance(transitionalCameraInfo.maxZoomDistance);
|
||||
|
||||
self:SetZoomDistance(transitionalCameraInfo.zoomDistance);
|
||||
|
||||
self:SetYaw(transitionalCameraInfo.yaw);
|
||||
self:SetPitch(transitionalCameraInfo.pitch);
|
||||
self:SetRoll(transitionalCameraInfo.roll);
|
||||
|
||||
if transitionType == CAMERA_TRANSITION_TYPE_IMMEDIATE then
|
||||
self:SnapAllInterpolatedValues();
|
||||
end
|
||||
self:UpdateCameraOrientationAndPosition();
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetTarget(x, y, z)
|
||||
self.targetX, self.targetY, self.targetZ = x, y, z;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetTarget(x, y, z)
|
||||
return self.targetX, self.targetY, self.targetZ;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetYaw(yaw)
|
||||
self.yaw = yaw;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetYaw()
|
||||
return self.yaw;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetPitch(pitch)
|
||||
self.pitch = pitch;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetPitch()
|
||||
return self.pitch;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetRoll(roll)
|
||||
self.roll = roll;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetRoll()
|
||||
return self.roll;
|
||||
end
|
||||
|
||||
-- where 100% is fully zoomed out
|
||||
function OrbitCameraMixin:GetZoomPercent()
|
||||
return self.zoomPercent;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetZoomPercent(zoomPercent)
|
||||
self.zoomPercent = Saturate(zoomPercent);
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:ZoomByPercent(percent)
|
||||
self:SetZoomPercent(self:GetZoomPercent() - percent);
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetMaxZoomDistance(distance)
|
||||
self.maxZoomDistance = distance;
|
||||
if self:GetZoomPercent() then
|
||||
self:SetZoomDistance(self:GetZoomDistance());
|
||||
end
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetMaxZoomDistance()
|
||||
return self.maxZoomDistance;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetMinZoomDistance(distance)
|
||||
self.minZoomDistance = distance;
|
||||
if self:GetZoomPercent() then
|
||||
self:SetZoomDistance(self:GetZoomDistance());
|
||||
end
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetMinZoomDistance()
|
||||
return self.minZoomDistance;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetZoomDistance(distance)
|
||||
self:SetZoomPercent(self:CalculateZoomPercentFromDistance(distance));
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:CalculateZoomPercentFromDistance(distance)
|
||||
if self:GetMinZoomDistance() and self:GetMaxZoomDistance() then
|
||||
return PercentageBetween(distance, self:GetMinZoomDistance(), self:GetMaxZoomDistance());
|
||||
end
|
||||
return 0.0;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:CalculateZoomDistanceFromPercent(percent)
|
||||
if self:GetMinZoomDistance() and self:GetMaxZoomDistance() then
|
||||
return Lerp(self:GetMinZoomDistance(), self:GetMaxZoomDistance(), percent);
|
||||
end
|
||||
return 0.0;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetZoomDistance()
|
||||
return self:CalculateZoomDistanceFromPercent(self:GetZoomPercent());
|
||||
end
|
||||
|
||||
-- If min and max values are the same, zoom cannot really happen.
|
||||
function OrbitCameraMixin:GetZoomAvailable()
|
||||
if self:GetMinZoomDistance() and self:GetMaxZoomDistance() then
|
||||
return self:GetMinZoomDistance() ~= self:GetMaxZoomDistance();
|
||||
end
|
||||
return false;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:ZoomBy(distance)
|
||||
self:SetZoomDistance(self:GetZoomDistance() - distance);
|
||||
end
|
||||
|
||||
-- Expects a three dimensional spline where a point of the curve represents a target position
|
||||
function OrbitCameraMixin:SetTargetSpline(targetSpline)
|
||||
self.targetSpline = targetSpline;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetTargetSpline()
|
||||
return self.targetSpline;
|
||||
end
|
||||
|
||||
-- Expects a three dimensional spline where a point of the curve represents the camera yaw, pitch, roll
|
||||
function OrbitCameraMixin:SetOrientationSpline(orientationSpline)
|
||||
self.orientationSpline = orientationSpline;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetOrientationSpline()
|
||||
return self.orientationSpline;
|
||||
end
|
||||
|
||||
-- Expects an one dimensional spline where a point of the curve represents the camera's distance from the target
|
||||
function OrbitCameraMixin:SetZoomSpline(zoomSpline)
|
||||
self.zoomSpline = zoomSpline;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetZoomSpline()
|
||||
return self.zoomSpline;
|
||||
end
|
||||
|
||||
--[[ These return the simple + spline values ]]--
|
||||
function OrbitCameraMixin:GetDerivedTarget()
|
||||
local targetX, targetY, targetZ = self:GetTarget();
|
||||
|
||||
local targetSpline = self:GetTargetSpline();
|
||||
if targetSpline then
|
||||
return Vector3D_Add(targetX, targetY, targetZ, targetSpline:CalculatePointOnGlobalCurve(1.0 - self:GetZoomPercent()));
|
||||
end
|
||||
|
||||
return targetX, targetY, targetZ;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetDerivedOrientation()
|
||||
local yaw, pitch, roll = self:GetYaw(), self:GetPitch(), self:GetRoll();
|
||||
|
||||
local orientationSpline = self:GetOrientationSpline();
|
||||
if orientationSpline then
|
||||
return Vector3D_Add(yaw, pitch, roll, orientationSpline:CalculatePointOnGlobalCurve(1.0 - self:GetZoomPercent()));
|
||||
end
|
||||
|
||||
return yaw, pitch, roll;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetDerivedZoomDistance()
|
||||
local zoomDistance = self:GetZoomDistance();
|
||||
|
||||
local zoomSpline = self:GetZoomSpline();
|
||||
if zoomSpline then
|
||||
return zoomDistance + zoomSpline:CalculatePointOnGlobalCurve(1.0 - self:GetZoomPercent());
|
||||
end
|
||||
|
||||
return zoomDistance;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetAlignLightToOrbitDelta(alignLightToOrbitDelta)
|
||||
if alignLightToOrbitDelta then
|
||||
self.modelSceneCameraInfo.flags = bit.bor(self.modelSceneCameraInfo.flags, Enum.ModelSceneSetting.AlignLightToOrbitDelta);
|
||||
else
|
||||
self.modelSceneCameraInfo.flags = bit.band(self.modelSceneCameraInfo.flags, bit.bnot(Enum.ModelSceneSetting.AlignLightToOrbitDelta));
|
||||
end
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:ShouldAlignLightToOrbitDelta()
|
||||
return bit.band(self.modelSceneCameraInfo.flags, Enum.ModelSceneSetting.AlignLightToOrbitDelta) == Enum.ModelSceneSetting.AlignLightToOrbitDelta;
|
||||
end
|
||||
|
||||
--[[
|
||||
Interpolation API
|
||||
For each API, "amount" is the percentage to approach in an "ideal" frame (60 fps), such that setting the value to >= 1 would snap to the desired target and <= 0 would freeze the interpolation.
|
||||
Setting to nil will disable the interpolation.
|
||||
]]
|
||||
|
||||
function OrbitCameraMixin:SetYawInterpolationAmount(yawInterpolationAmount)
|
||||
self.yawInterpolationAmount = yawInterpolationAmount;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetYawInterpolationAmount()
|
||||
return self.yawInterpolationAmount;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetPitchInterpolationAmount(pitchInterpolationAmount)
|
||||
self.pitchInterpolationAmount = pitchInterpolationAmount;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetPitchInterpolationAmount()
|
||||
return self.pitchInterpolationAmount;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetRollInterpolationAmount(rollInterpolationAmount)
|
||||
self.rollInterpolationAmount = rollInterpolationAmount;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetRollInterpolationAmount()
|
||||
return self.rollInterpolationAmount;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetTargetInterpolationAmount(targetInterpolationAmount)
|
||||
self.targetInterpolationAmount = targetInterpolationAmount;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetTargetInterpolationAmount()
|
||||
return self.targetInterpolationAmount;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetZoomInterpolationAmount(zoomInterpolationAmount)
|
||||
self.zoomInterpolationAmount = zoomInterpolationAmount;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetZoomInterpolationAmount()
|
||||
return self.zoomInterpolationAmount;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetInterpolatedTarget()
|
||||
if self.interpolatedTargetX then
|
||||
return self.interpolatedTargetX, self.interpolatedTargetY, self.interpolatedTargetZ;
|
||||
end
|
||||
return self:GetDerivedTarget();
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetInterpolatedOrientation()
|
||||
if self.interpolatedYaw then
|
||||
return self.interpolatedYaw, self.interpolatedPitch, self.interpolatedRoll;
|
||||
end
|
||||
return self:GetDerivedOrientation();
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetInterpolatedZoomDistance()
|
||||
if self.interpolatedZoomDistance then
|
||||
return self.interpolatedZoomDistance;
|
||||
end
|
||||
return self:GetDerivedZoomDistance();
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SnapToTargetInterpolationYaw()
|
||||
self.interpolatedYaw = nil;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SnapToTargetInterpolationPitch()
|
||||
self.interpolatedPitch = nil;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SnapToTargetInterpolationRoll()
|
||||
self.interpolatedRoll = nil;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SnapToTargetInterpolationZoom()
|
||||
self.interpolatedZoomDistance = nil;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SnapToTargetInterpolationTarget()
|
||||
self.interpolatedTargetX = nil;
|
||||
self.interpolatedTargetY = nil;
|
||||
self.interpolatedTargetZ = nil;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SnapAllInterpolatedValues()
|
||||
self:SnapToTargetInterpolationYaw();
|
||||
self:SnapToTargetInterpolationPitch();
|
||||
self:SnapToTargetInterpolationRoll();
|
||||
|
||||
self:SnapToTargetInterpolationZoom();
|
||||
|
||||
self:SnapToTargetInterpolationTarget();
|
||||
end
|
||||
|
||||
ORBIT_CAMERA_MOUSE_MODE_NOTHING = 0;
|
||||
ORBIT_CAMERA_MOUSE_MODE_YAW_ROTATION = 1;
|
||||
ORBIT_CAMERA_MOUSE_MODE_PITCH_ROTATION = 2;
|
||||
ORBIT_CAMERA_MOUSE_MODE_ROLL_ROTATION = 3;
|
||||
ORBIT_CAMERA_MOUSE_MODE_TARGET_HORIZONTAL = 4;
|
||||
ORBIT_CAMERA_MOUSE_MODE_TARGET_VERTICAL = 5;
|
||||
ORBIT_CAMERA_MOUSE_MODE_ZOOM = 6;
|
||||
ORBIT_CAMERA_MOUSE_PAN_HORIZONTAL = 7;
|
||||
ORBIT_CAMERA_MOUSE_PAN_VERTICAL = 8;
|
||||
|
||||
function OrbitCameraMixin:SetLeftMouseButtonXMode(mouseMode, snap)
|
||||
self.buttonModes.leftX = mouseMode;
|
||||
self.buttonModes.leftXinterpolate = not snap;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetLeftMouseButtonXMode()
|
||||
return self.buttonModes.leftX, not self.buttonModes.leftXinterpolate;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetLeftMouseButtonYMode(mouseMode, snap)
|
||||
self.buttonModes.leftY = mouseMode;
|
||||
self.buttonModes.leftYinterpolate = not snap;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetLeftYMouseButtonYMode()
|
||||
return self.buttonModes.leftY, not self.buttonModes.leftYinterpolate;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetRightMouseButtonXMode(mouseMode, snap)
|
||||
self.buttonModes.rightX = mouseMode;
|
||||
self.buttonModes.rightXinterpolate = not snap;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetRightMouseButtonXMode()
|
||||
return self.buttonModes.rightX, not self.buttonModes.rightXinterpolate;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetRightMouseButtonYMode(mouseMode, snap)
|
||||
self.buttonModes.rightY = mouseMode;
|
||||
self.buttonModes.rightYinterpolate = not snap;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetRightMouseButtonYMode()
|
||||
return self.buttonModes.rightY, not self.buttonModes.rightYinterpolate;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetMouseWheelMode(mouseMode, snap)
|
||||
self.buttonModes.wheel = mouseMode;
|
||||
self.buttonModes.wheelInterpolate = not snap;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetMouseWheelMode()
|
||||
return self.buttonModes.wheel, not self.buttonModes.wheelInterpolate;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:HandleMouseMovement(mode, delta, snapToValue)
|
||||
if mode == ORBIT_CAMERA_MOUSE_MODE_YAW_ROTATION then
|
||||
self:SetYaw(self:GetYaw() - delta);
|
||||
if snapToValue then
|
||||
self:SnapToTargetInterpolationYaw();
|
||||
end
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_MODE_PITCH_ROTATION then
|
||||
self:SetPitch(self:GetPitch() - delta);
|
||||
if snapToValue then
|
||||
self:SnapToTargetInterpolationPitch();
|
||||
end
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_MODE_ROLL_ROTATION then
|
||||
self:SetRoll(self:GetRoll() - delta);
|
||||
if snapToValue then
|
||||
self:SnapToTargetInterpolationRoll();
|
||||
end
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_MODE_ZOOM then
|
||||
self:ZoomByPercent(delta);
|
||||
if snapToValue then
|
||||
self:SnapToTargetInterpolationZoom();
|
||||
end
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_MODE_TARGET_HORIZONTAL then
|
||||
local rightX, rightY, rightZ = Vector3D_ScaleBy(delta, self:GetRightVector());
|
||||
self:SetTarget(Vector3D_Add(rightX, rightY, rightZ, self:GetTarget()));
|
||||
|
||||
if snapToValue then
|
||||
self:SnapToTargetInterpolationTarget();
|
||||
end
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_MODE_TARGET_VERTICAL then
|
||||
local upX, upY, upZ = Vector3D_ScaleBy(delta, self:GetUpVector());
|
||||
self:SetTarget(Vector3D_Add(upX, upY, upZ, self:GetTarget()));
|
||||
|
||||
if snapToValue then
|
||||
self:SnapToTargetInterpolationTarget();
|
||||
end
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_PAN_HORIZONTAL then
|
||||
self.panningXOffset = self.panningXOffset + delta;
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_PAN_VERTICAL then
|
||||
self.panningYOffset = self.panningYOffset + delta;
|
||||
end
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:ResetDefaultInputModes()
|
||||
self:SetLeftMouseButtonXMode(ORBIT_CAMERA_MOUSE_MODE_YAW_ROTATION, true);
|
||||
self:SetLeftMouseButtonYMode(ORBIT_CAMERA_MOUSE_MODE_NOTHING);
|
||||
self:SetRightMouseButtonXMode(ORBIT_CAMERA_MOUSE_MODE_NOTHING);
|
||||
self:SetRightMouseButtonYMode(ORBIT_CAMERA_MOUSE_MODE_NOTHING);
|
||||
self:SetMouseWheelMode(ORBIT_CAMERA_MOUSE_MODE_ZOOM, false);
|
||||
end
|
||||
|
||||
-- "private" function
|
||||
function OrbitCameraMixin:OnAdded() -- override
|
||||
self.buttonModes = {};
|
||||
|
||||
self:SetTarget(0, 0, 0);
|
||||
|
||||
local targetSpline = CreateCatmullRomSpline(3);
|
||||
targetSpline:AddPoint(0, 0, 0);
|
||||
targetSpline:AddPoint(0, 0, .5);
|
||||
|
||||
self:SetTargetSpline(targetSpline);
|
||||
|
||||
self:SetMinZoomDistance(6);
|
||||
self:SetMaxZoomDistance(10);
|
||||
|
||||
self:SetZoomDistance(8);
|
||||
|
||||
self:SetYaw(math.pi);
|
||||
self:SetPitch(0);
|
||||
self:SetRoll(0);
|
||||
|
||||
self:SetZoomInterpolationAmount(.15);
|
||||
self:SetYawInterpolationAmount(.15);
|
||||
self:SetPitchInterpolationAmount(.15);
|
||||
self:SetRollInterpolationAmount(.15);
|
||||
self:SetTargetInterpolationAmount(.15);
|
||||
|
||||
self:ResetDefaultInputModes();
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetDeltaModifierForCameraMode(mode)
|
||||
if mode == ORBIT_CAMERA_MOUSE_MODE_YAW_ROTATION then
|
||||
return .008;
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_MODE_PITCH_ROTATION then
|
||||
return .008;
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_MODE_ROLL_ROTATION then
|
||||
return .008;
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_MODE_ZOOM then
|
||||
return .1;
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_MODE_TARGET_HORIZONTAL then
|
||||
return .05;
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_MODE_TARGET_VERTICAL then
|
||||
return .05;
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_PAN_HORIZONTAL then
|
||||
return 0.93;
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_PAN_VERTICAL then
|
||||
return 0.93;
|
||||
end
|
||||
return 0.0;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:OnUpdate(elapsed) -- override
|
||||
if self:IsLeftMouseButtonDown() then
|
||||
local deltaX, deltaY = InputUtil.GetCursorDelta(GetAppropriateTopLevelParent());
|
||||
self:HandleMouseMovement(self.buttonModes.leftX, deltaX * self:GetDeltaModifierForCameraMode(self.buttonModes.leftX), not self.buttonModes.leftXinterpolate);
|
||||
self:HandleMouseMovement(self.buttonModes.leftY, -deltaY * self:GetDeltaModifierForCameraMode(self.buttonModes.leftY), not self.buttonModes.leftYinterpolate);
|
||||
end
|
||||
|
||||
if self:IsRightMouseButtonDown() then
|
||||
local deltaX, deltaY = InputUtil.GetCursorDelta(GetAppropriateTopLevelParent());
|
||||
self:HandleMouseMovement(self.buttonModes.rightX, deltaX * self:GetDeltaModifierForCameraMode(self.buttonModes.rightX), not self.buttonModes.rightXinterpolate);
|
||||
self:HandleMouseMovement(self.buttonModes.rightY, -deltaY * self:GetDeltaModifierForCameraMode(self.buttonModes.rightY), not self.buttonModes.rightYinterpolate);
|
||||
end
|
||||
|
||||
self:UpdateInterpolationTargets(elapsed);
|
||||
self:SynchronizeCamera();
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SynchronizeCamera() -- override
|
||||
self:UpdateCameraOrientationAndPosition();
|
||||
self:UpdateLight();
|
||||
end
|
||||
|
||||
local function InterpolateDimension(lastValue, targetValue, amount, elapsed)
|
||||
return lastValue and amount and DeltaLerp(lastValue, targetValue, amount, elapsed) or targetValue;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:UpdateInterpolationTargets(elapsed)
|
||||
local yaw, pitch, roll = self:GetDerivedOrientation();
|
||||
local targetX, targetY, targetZ = self:GetDerivedTarget();
|
||||
local zoomDistance = self:GetDerivedZoomDistance();
|
||||
|
||||
self.interpolatedYaw = InterpolateDimension(self.interpolatedYaw, yaw, self.yawInterpolationAmount, elapsed);
|
||||
self.interpolatedPitch = InterpolateDimension(self.interpolatedPitch, pitch, self.pitchInterpolationAmount, elapsed);
|
||||
self.interpolatedRoll = InterpolateDimension(self.interpolatedRoll, roll, self.rollInterpolationAmount, elapsed);
|
||||
|
||||
self.interpolatedZoomDistance = InterpolateDimension(self.interpolatedZoomDistance, zoomDistance, self.zoomInterpolationAmount, elapsed);
|
||||
|
||||
self.interpolatedTargetX = InterpolateDimension(self.interpolatedTargetX, targetX, self.targetInterpolationAmount, elapsed);
|
||||
self.interpolatedTargetY = InterpolateDimension(self.interpolatedTargetY, targetY, self.targetInterpolationAmount, elapsed);
|
||||
self.interpolatedTargetZ = InterpolateDimension(self.interpolatedTargetZ, targetZ, self.targetInterpolationAmount, elapsed);
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:UpdateCameraOrientationAndPosition()
|
||||
local yaw, pitch, roll = self:GetInterpolatedOrientation();
|
||||
local axisAngleX, axisAngleY, axisAngleZ = Vector3D_CalculateNormalFromYawPitch(yaw, pitch);
|
||||
|
||||
local targetX, targetY, targetZ = self:GetInterpolatedTarget();
|
||||
|
||||
local zoomDistance = self:GetInterpolatedZoomDistance();
|
||||
|
||||
-- Panning start --
|
||||
-- We want the model to move 1-to-1 with the mouse.
|
||||
-- Panning formula: dx / hypotenuse * (zoomDistance - 1 / zoomDistance^3)
|
||||
-- It was experimentally determined that adding the additional fudge factor 1/z^3 resulted in better tracking.
|
||||
|
||||
local width = self:GetOwningScene():GetWidth();
|
||||
local height = self:GetOwningScene():GetHeight();
|
||||
local scaleFactor = (width ~= 0 and height ~= 0) and math.sqrt(width * width + height * height) or 1;
|
||||
local zoomFactor = 1;
|
||||
if zoomDistance > 1 then
|
||||
zoomFactor = zoomDistance - (1 / (zoomDistance * zoomDistance * zoomDistance));
|
||||
if zoomFactor < 1 then
|
||||
zoomFactor = 1;
|
||||
end
|
||||
end
|
||||
|
||||
local rightX, rightY, rightZ = Vector3D_ScaleBy((self.panningXOffset / scaleFactor) * zoomFactor, self:GetRightVector());
|
||||
local upX, upY, upZ = Vector3D_ScaleBy((self.panningYOffset / scaleFactor) * zoomFactor, self:GetUpVector());
|
||||
|
||||
-- Panning end --
|
||||
|
||||
targetX, targetY, targetZ = Vector3D_Add(targetX, targetY, targetZ, rightX, rightY, rightZ);
|
||||
targetX, targetY, targetZ = Vector3D_Add(targetX, targetY, targetZ, upX, upY, upZ);
|
||||
|
||||
self:SetPosition(self:CalculatePositionByDistanceFromTarget(targetX, targetY, targetZ, zoomDistance, axisAngleX, axisAngleY, axisAngleZ));
|
||||
self:GetOwningScene():SetCameraOrientationByYawPitchRoll(yaw, pitch, roll);
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:UpdateLight()
|
||||
if self:ShouldAlignLightToOrbitDelta() then
|
||||
local owningScene = self:GetOwningScene();
|
||||
owningScene:SetLightDirection(self:GetForwardVector());
|
||||
end
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:CalculatePositionByDistanceFromTarget(targetX, targetY, targetZ, zoomDistance, axisAngleX, axisAngleY, axisAngleZ)
|
||||
local towardsPosX, towardsPosY, towardsPosZ = Vector3D_ScaleBy(-zoomDistance, axisAngleX, axisAngleY, axisAngleZ);
|
||||
return Vector3D_Add(towardsPosX, towardsPosY, towardsPosZ, targetX, targetY, targetZ);
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:CalculateTransitionalValues(fromModelSceneCameraInfo, toModelSceneCameraInfo, modificationType)
|
||||
if fromModelSceneCameraInfo and modificationType == CAMERA_MODIFICATION_TYPE_MAINTAIN then
|
||||
local zoomedTargetOffsetX, zoomedTargetOffsetY, zoomedTargetOffsetZ = 0, 0, 0;
|
||||
|
||||
local targetSpline = self:GetTargetSpline();
|
||||
if targetSpline and targetSpline:GetNumPoints() > 0 then
|
||||
zoomedTargetOffsetX, zoomedTargetOffsetY, zoomedTargetOffsetZ = targetSpline:GetPoint(targetSpline:GetNumPoints());
|
||||
end
|
||||
|
||||
local zoomedYawOffset, zoomedPitchOffset, zoomedRollOffset = 0, 0, 0;
|
||||
local orientationSpline = self:GetOrientationSpline();
|
||||
if orientationSpline and orientationSpline:GetNumPoints() > 0 then
|
||||
zoomedYawOffset, zoomedPitchOffset, zoomedRollOffset = orientationSpline:GetPoint(orientationSpline:GetNumPoints());
|
||||
end
|
||||
|
||||
local fromZoomDistancePercent = PercentageBetween(fromModelSceneCameraInfo.zoomDistance, fromModelSceneCameraInfo.minZoomDistance, fromModelSceneCameraInfo.maxZoomDistance);
|
||||
local toZoomDistancePercent = PercentageBetween(toModelSceneCameraInfo.zoomDistance, toModelSceneCameraInfo.minZoomDistance, toModelSceneCameraInfo.maxZoomDistance);
|
||||
-- Maintain any modifications made by applying the scene delta
|
||||
return {
|
||||
target = CreateVector3D(
|
||||
(self.targetX - fromModelSceneCameraInfo.target.x) + toModelSceneCameraInfo.target.x,
|
||||
(self.targetY - fromModelSceneCameraInfo.target.y) + toModelSceneCameraInfo.target.y,
|
||||
(self.targetZ - fromModelSceneCameraInfo.target.z) + toModelSceneCameraInfo.target.z
|
||||
),
|
||||
|
||||
yaw = (self.yaw - fromModelSceneCameraInfo.yaw) + toModelSceneCameraInfo.yaw,
|
||||
pitch = (self.pitch - fromModelSceneCameraInfo.pitch) + toModelSceneCameraInfo.pitch,
|
||||
roll = (self.roll - fromModelSceneCameraInfo.roll) + toModelSceneCameraInfo.roll,
|
||||
|
||||
zoomDistance = Lerp(toModelSceneCameraInfo.minZoomDistance, toModelSceneCameraInfo.maxZoomDistance, (self.zoomPercent - fromZoomDistancePercent) + toZoomDistancePercent),
|
||||
minZoomDistance = (self.minZoomDistance - fromModelSceneCameraInfo.minZoomDistance) + toModelSceneCameraInfo.minZoomDistance,
|
||||
maxZoomDistance = (self.maxZoomDistance - fromModelSceneCameraInfo.maxZoomDistance) + toModelSceneCameraInfo.maxZoomDistance,
|
||||
|
||||
zoomedTargetOffset = CreateVector3D(
|
||||
(zoomedTargetOffsetX - fromModelSceneCameraInfo.zoomedTargetOffset.x) + toModelSceneCameraInfo.zoomedTargetOffset.x,
|
||||
(zoomedTargetOffsetY - fromModelSceneCameraInfo.zoomedTargetOffset.y) + toModelSceneCameraInfo.zoomedTargetOffset.y,
|
||||
(zoomedTargetOffsetZ - fromModelSceneCameraInfo.zoomedTargetOffset.z) + toModelSceneCameraInfo.zoomedTargetOffset.z
|
||||
),
|
||||
|
||||
zoomedYawOffset = (zoomedYawOffset - fromModelSceneCameraInfo.zoomedYawOffset) + toModelSceneCameraInfo.zoomedYawOffset,
|
||||
zoomedPitchOffset = (zoomedPitchOffset - fromModelSceneCameraInfo.zoomedPitchOffset) + toModelSceneCameraInfo.zoomedPitchOffset,
|
||||
zoomedRollOffset = (zoomedRollOffset - fromModelSceneCameraInfo.zoomedRollOffset) + toModelSceneCameraInfo.zoomedRollOffset;
|
||||
};
|
||||
end
|
||||
|
||||
-- Nothing to transition from, just go directly to the values
|
||||
return toModelSceneCameraInfo;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:OnMouseWheel(delta) -- override
|
||||
self:HandleMouseMovement(self.buttonModes.wheel, delta * self:GetDeltaModifierForCameraMode(self.buttonModes.wheel), not self.buttonModes.wheelInterpolate);
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:AdjustYaw(deltaX, deltaY, rotationScalar)
|
||||
local xRotation = rotationScalar or self:GetDeltaModifierForCameraMode(self.buttonModes.leftX);
|
||||
self:HandleMouseMovement(self.buttonModes.leftX, deltaX * xRotation, not self.buttonModes.leftXinterpolate);
|
||||
|
||||
local yRotatation = rotationScalar or self:GetDeltaModifierForCameraMode(self.buttonModes.leftY);
|
||||
self:HandleMouseMovement(self.buttonModes.leftY, -deltaY * yRotatation, not self.buttonModes.leftYinterpolate);
|
||||
end
|
||||
@@ -23,15 +23,15 @@
|
||||
-- - The text is a ScrollingEditBoxTemplate with a MinimalScrollBar, the
|
||||
-- ScrollBox pair retail uses for scrolling edit boxes, not retail's
|
||||
-- ScrollFrameTemplate, which ClassicAPI does not port.
|
||||
-- - The stack is retail's GetErrorData without the locals, taken in the
|
||||
-- SetText slot: GetErrorCallstackHeight finds the code that failed
|
||||
-- however many handlers sit between it and the slot. 1.12 has no
|
||||
-- debuglocals.
|
||||
-- - The stack and locals are retail's GetErrorData, taken in the SetText
|
||||
-- slot: GetErrorCallstackHeight finds the code that failed however many
|
||||
-- handlers sit between it and the slot. No |K strings to remove, and no
|
||||
-- C_StringUtil.EscapeDecimalNonPrintables.
|
||||
-- - No scriptErrors cvar (1.12 always shows errors), no LUA_WARNING, no
|
||||
-- gamepad legend.
|
||||
-- - Lua 5.0: no `...` expression, no # operator.
|
||||
|
||||
local function GetErrorStack()
|
||||
local function GetErrorData()
|
||||
-- Example of how debug stack level is calculated
|
||||
-- Current stack: [1, 2, 3, 4, 5] (current function is at 1, total current height is 5)
|
||||
-- Stack at time of error: [1, 2] (these are currently now index 4 and 5, but at the time of error the stack height is 2)
|
||||
@@ -40,13 +40,20 @@ local function GetErrorStack()
|
||||
local errorCallStackHeight = GetErrorCallstackHeight();
|
||||
local errorStackOffset = errorCallStackHeight and (errorCallStackHeight - 1);
|
||||
local debugStackLevel = currentStackHeight - (errorStackOffset or 0);
|
||||
return debugstack(debugStackLevel);
|
||||
local skipFunctionsAndUserdata = true;
|
||||
|
||||
local stack = debugstack(debugStackLevel);
|
||||
local locals = debuglocals(debugStackLevel, skipFunctionsAndUserdata);
|
||||
|
||||
return stack, locals;
|
||||
end
|
||||
|
||||
local ERROR_FORMAT = "|cffffd200Message:|r|cffffffff %s|r\n"
|
||||
.. "|cffffd200Time:|r|cffffffff %s|r\n"
|
||||
.. "|cffffd200Count:|r|cffffffff %s|r\n"
|
||||
.. "|cffffd200Stack:|r\n"
|
||||
.. "|cffffffff%s|r\n"
|
||||
.. "|cffffd200Locals:|r\n"
|
||||
.. "|cffffffff%s|r";
|
||||
|
||||
local INDEX_ORDER_FORMAT = "%d / %d";
|
||||
@@ -68,15 +75,16 @@ function ScriptErrorsFrame_OnLoad(self)
|
||||
ScrollUtil.RegisterScrollBoxWithScrollBar(scrollBox, self.ScrollBar);
|
||||
|
||||
ScriptErrors_Message.SetText = function(_, message)
|
||||
ScriptErrorsFrame_OnError(message, GetErrorStack());
|
||||
ScriptErrorsFrame_OnError(message, GetErrorData());
|
||||
end
|
||||
ScriptErrors.Show = function() end
|
||||
end
|
||||
|
||||
function ScriptErrorsFrame_OnError(message, stack)
|
||||
function ScriptErrorsFrame_OnError(message, stack, locals)
|
||||
local self = ScriptErrorsFrame;
|
||||
message = tostring(message);
|
||||
stack = stack or "";
|
||||
locals = locals or "<none>";
|
||||
|
||||
local messageKey = message .. "\n" .. stack;
|
||||
local index = self.seen[messageKey];
|
||||
@@ -84,7 +92,7 @@ function ScriptErrorsFrame_OnError(message, stack)
|
||||
local errorData = self.errorData[index];
|
||||
errorData.count = errorData.count + 1;
|
||||
else
|
||||
table.insert(self.errorData, { count = 1, message = message, time = date(), stack = stack });
|
||||
table.insert(self.errorData, { count = 1, message = message, time = date(), stack = stack, locals = locals });
|
||||
index = table.getn(self.errorData);
|
||||
self.seen[messageKey] = index;
|
||||
end
|
||||
@@ -106,7 +114,7 @@ function ScriptErrorsFrame_Update()
|
||||
-- SetText puts the cursor and the scroll back at the top.
|
||||
local errorData = self.errorData[self.index];
|
||||
if ( errorData ) then
|
||||
self.ScrollingEditBox:SetText(string.format(ERROR_FORMAT, errorData.message, errorData.time, errorData.count, errorData.stack));
|
||||
self.ScrollingEditBox:SetText(string.format(ERROR_FORMAT, errorData.message, errorData.time, errorData.count, errorData.stack, errorData.locals));
|
||||
self.ScrollingEditBox:GetEditBox():HighlightText(0);
|
||||
else
|
||||
self.ScrollingEditBox:ClearText();
|
||||
|
||||
@@ -607,6 +607,17 @@ AddKeybinding(bags, "AOELOOT")
|
||||
|
||||
local interface = NewPage(CLASSICAPI_INTERFACE, main.name)
|
||||
|
||||
-- The label ends with some of the cursors the setting shows. The DLL serves
|
||||
-- each image at its Interface\Cursor path, and the markup stays out of the
|
||||
-- translated string.
|
||||
local expandedCursorIcons = ""
|
||||
for _, name in ipairs({ "RepairNPC", "Innkeeper", "StableMaster", "Quest", "QuestTurnIn", "Fishing" }) do
|
||||
expandedCursorIcons = expandedCursorIcons .. " |TInterface\\Cursor\\" .. name .. ":16|t"
|
||||
end
|
||||
|
||||
AddHeader(interface, CLASSICAPI_CURSOR)
|
||||
AddCheckbox(interface, CLASSICAPI_EXPANDED_CURSORS .. expandedCursorIcons, CLASSICAPI_EXPANDED_CURSORS_TOOLTIP, CVarSetting("expandedCursors"))
|
||||
|
||||
AddHeader(interface, CLASSICAPI_NAMEPLATES)
|
||||
AddSlider(interface, CLASSICAPI_TARGET_NAMEPLATE_ALPHA, CLASSICAPI_TARGET_NAMEPLATE_ALPHA_TOOLTIP, NumberCVarSetting("nameplateSelectedAlpha"), 0, 1, 0.05)
|
||||
AddSlider(interface, CLASSICAPI_OTHER_NAMEPLATE_ALPHA, CLASSICAPI_OTHER_NAMEPLATE_ALPHA_TOOLTIP, NumberCVarSetting("nameplateNotSelectedAlpha"), 0, 1, 0.05)
|
||||
|
||||
@@ -107,6 +107,12 @@ function GameTooltip_AddNormalLine(tooltip, text, wrap, leftOffset)
|
||||
GameTooltip_AddColoredLine(tooltip, text, NORMAL_FONT_COLOR, wrap, leftOffset);
|
||||
end
|
||||
|
||||
-- Retail's takes `...` and passes it on; named parameters keep Lua 5.0 from
|
||||
-- building an `arg` table per call.
|
||||
function GameTooltip_AddBodyLine(tooltip, text, wrap, leftOffset)
|
||||
GameTooltip_AddNormalLine(tooltip, text, wrap, leftOffset);
|
||||
end
|
||||
|
||||
function GameTooltip_AddHighlightLine(tooltip, text, wrap, leftOffset)
|
||||
GameTooltip_AddColoredLine(tooltip, text, HIGHLIGHT_FONT_COLOR, wrap, leftOffset);
|
||||
end
|
||||
|
||||
@@ -8,6 +8,12 @@ function InputUtil.GetCursorPosition(parent)
|
||||
return x / scale, y / scale;
|
||||
end
|
||||
|
||||
function InputUtil.GetCursorDelta(parent)
|
||||
local x, y = GetCursorDelta();
|
||||
local scale = parent:GetEffectiveScale();
|
||||
return x / scale, y / scale;
|
||||
end
|
||||
|
||||
function InputUtil.AnchorRegionToCursor(region, point)
|
||||
local parent = GetAppropriateTopLevelParent();
|
||||
local x, y = InputUtil.GetCursorPosition(parent);
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
-- (scrapping, runeforging, soulbinds, keystones, item upgrades,
|
||||
-- recrafting, bank tabs), and the stock bag and character frames never
|
||||
-- ask for it.
|
||||
-- - No ItemUtil.CreateItemTransmogInfo or ItemTransmogInfoMixin: there is
|
||||
-- no transmogrification.
|
||||
-- - ItemTransmogInfoMixin's appearanceID is an item ID and its illusionID a
|
||||
-- SpellItemEnchantment ID, as the ModelSceneActor ItemTransmogInfo
|
||||
-- methods read and write them.
|
||||
-- - ItemUtil.GetItemDetails has no currency path: there are no currencies.
|
||||
-- - No ItemUtil.GetCraftingReagentCount: it picks between the reagent bank
|
||||
-- and the account bank, and neither exists.
|
||||
@@ -95,6 +96,10 @@ function ItemUtil.FilterOwnedItems(itemIDs)
|
||||
return filtered;
|
||||
end
|
||||
|
||||
function ItemUtil.CreateItemTransmogInfo(appearanceID, secondaryAppearanceID, illusionID)
|
||||
return CreateAndInitFromMixin(ItemTransmogInfoMixin, appearanceID, secondaryAppearanceID, illusionID);
|
||||
end
|
||||
|
||||
function ItemUtil.TransformItemIDsToItems(itemIDs)
|
||||
local items = {};
|
||||
for index, itemID in ipairs(itemIDs) do
|
||||
@@ -194,3 +199,42 @@ function ItemUtil.GetEquipSlotTexture(equipSlot)
|
||||
|
||||
return nil;
|
||||
end
|
||||
|
||||
ItemTransmogInfoMixin = {};
|
||||
|
||||
function ItemTransmogInfoMixin:Init(appearanceID, secondaryAppearanceID, illusionID)
|
||||
self.appearanceID = appearanceID;
|
||||
self.secondaryAppearanceID = secondaryAppearanceID or Constants.Transmog.NoTransmogID;
|
||||
self.illusionID = illusionID or Constants.Transmog.NoTransmogID;
|
||||
end
|
||||
|
||||
function ItemTransmogInfoMixin:IsEqual(itemTransmogInfo)
|
||||
if not itemTransmogInfo then
|
||||
return false;
|
||||
end
|
||||
return self.appearanceID == itemTransmogInfo.appearanceID and self.secondaryAppearanceID == itemTransmogInfo.secondaryAppearanceID and self.illusionID == itemTransmogInfo.illusionID;
|
||||
end
|
||||
|
||||
function ItemTransmogInfoMixin:Clear()
|
||||
self.appearanceID = Constants.Transmog.NoTransmogID;
|
||||
self.secondaryAppearanceID = Constants.Transmog.NoTransmogID;
|
||||
self.illusionID = Constants.Transmog.NoTransmogID;
|
||||
end
|
||||
|
||||
-- There is no slot info in ItemTransmogInfo so the following 3 MainHand functions must be used with correct itemTransmogInfo at call site
|
||||
function ItemTransmogInfoMixin:ConfigureSecondaryForMainHand(isLegionArtifact)
|
||||
if isLegionArtifact then
|
||||
self.secondaryAppearanceID = Constants.Transmog.MainHandTransmogIsPairedWeapon;
|
||||
else
|
||||
self.secondaryAppearanceID = Constants.Transmog.MainHandTransmogIsIndividualWeapon;
|
||||
end
|
||||
end
|
||||
|
||||
function ItemTransmogInfoMixin:IsMainHandIndividualWeapon()
|
||||
return self.secondaryAppearanceID == Constants.Transmog.MainHandTransmogIsIndividualWeapon;
|
||||
end
|
||||
|
||||
function ItemTransmogInfoMixin:IsMainHandPairedWeapon()
|
||||
-- paired weapon can be value Constants.Transmog.MainHandTransmogIsPairedWeapon or greater
|
||||
return not self:IsMainHandIndividualWeapon();
|
||||
end
|
||||
|
||||
@@ -224,6 +224,9 @@ end
|
||||
-- extended `GameTooltip:SetHyperlink` (src/tooltip/Hyperlink.cpp), which
|
||||
-- builds the same tooltip `GameTooltip:SetSpellByID` builds, so the handler
|
||||
-- passes the payload through instead of rebuilding the tooltip itself.
|
||||
-- Control-click is retail's dress-up click: DressUpLink previews a mount
|
||||
-- spell, and nothing else happens, as retail's modified click shows no
|
||||
-- tooltip.
|
||||
------------------------------------------------------------------------------
|
||||
LinkUtil.RegisterLinkHandler(LinkTypes.Spell, function(link, text, linkData, contextData)
|
||||
local spellID = tonumber(string.match(link, "^spell:(%d+)"));
|
||||
@@ -234,6 +237,10 @@ LinkUtil.RegisterLinkHandler(LinkTypes.Spell, function(link, text, linkData, con
|
||||
ChatFrameEditBox:Insert(text);
|
||||
return LinkProcessorResponse.Handled;
|
||||
end
|
||||
if IsControlKeyDown() then
|
||||
DressUpLink(text);
|
||||
return LinkProcessorResponse.Handled;
|
||||
end
|
||||
local tooltip = ItemRefTooltip or GameTooltip;
|
||||
tooltip:SetOwner(UIParent, "ANCHOR_PRESERVE");
|
||||
tooltip:SetHyperlink(link);
|
||||
@@ -250,13 +257,14 @@ end);
|
||||
-- links. Route spell links Turtle would drop back through SetItemRef so the
|
||||
-- Spell handler above shows them; Turtle-shaped links
|
||||
-- (`spell:<id>:<bookType>:<player>`) pass through to Turtle's handler
|
||||
-- untouched. On a stock client the original already forwards everything to
|
||||
-- SetItemRef, so both branches behave the same there.
|
||||
-- untouched, except a Control-click, which previews the mount here. On a
|
||||
-- stock client the original already forwards everything to SetItemRef, so
|
||||
-- both branches behave the same there.
|
||||
------------------------------------------------------------------------------
|
||||
local originalChatFrameOnHyperlinkShow = ChatFrame_OnHyperlinkShow;
|
||||
function ChatFrame_OnHyperlinkShow(link, text, button)
|
||||
if type(link) == "string" and strsub(link, 1, 6) == "spell:" and
|
||||
not string.find(link, "^spell:(%d+):(%w+):(%w+)") then
|
||||
(IsControlKeyDown() or not string.find(link, "^spell:(%d+):(%w+):(%w+)")) then
|
||||
SetItemRef(link, text, button);
|
||||
return;
|
||||
end
|
||||
|
||||
@@ -8,6 +8,9 @@ BINDING_NAME_FOCUSTARGET = "Fokus setzen";
|
||||
BINDING_NAME_INTERACTMOUSEOVER = "Mit Mouseover interagieren";
|
||||
BINDING_NAME_INTERACTTARGET = "Mit Ziel interagieren";
|
||||
BINDING_NAME_TARGETFOCUS = "Fokus anvisieren";
|
||||
CLASSICAPI_CURSOR = "Mauszeiger";
|
||||
CLASSICAPI_EXPANDED_CURSORS = "Erweiterte Mauszeiger anzeigen";
|
||||
CLASSICAPI_EXPANDED_CURSORS_TOOLTIP = "Wenn Ihr mit der Maus auf einen NSC oder ein Objekt zeigt, stellt der Mauszeiger dar, was Ihr damit tun könnt. Zum Beispiel einen Amboss für Reparaturen, ! oder ? für Quests und einen Haken für Euren Angelschwimmer.";
|
||||
COMBAT_LABEL = "Kampf";
|
||||
COUNTDOWN_FOR_COOLDOWNS_TEXT = "Zahlen für Abklingzeiten anzeigen";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "Seid Ihr sicher, dass Ihr das Ausrüstungsset \"%s\" löschen wollt?";
|
||||
@@ -71,6 +74,10 @@ OPTION_TOOLTIP_INTERACT_ICONS = "Legt fest, welche Symbole über NSCs und Gegens
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Automatischen Angriff stoppen, sobald das Ziel gewechselt wird.";
|
||||
KEY_BINDING_NAME_AND_KEY = "%s (%s)";
|
||||
KEY_BINDING_TOOLTIP = "<Zum Löschen der Belegung rechtsklicken>";
|
||||
RESET_POSITION = "Zurücksetzen";
|
||||
ROTATE_LEFT = "Linksdrehung";
|
||||
ROTATE_RIGHT = "Rechtsdrehung";
|
||||
ROTATE_TOOLTIP = "Linksklicken und ziehen, um den Charakter zu drehen.";
|
||||
SETTINGS_BIND_KEY_TO_COMMAND_OR_CANCEL = "Tastaturbelegung für \"%s\" zuweisen oder mit %s abbrechen";
|
||||
SET_FOCUS = "Fokus setzen";
|
||||
FOCUSTARGET = "Ziel als Fokus festlegen";
|
||||
|
||||
@@ -13,15 +13,16 @@ CLASSICAPI_ACTION_TARGETING = "Action Targeting";
|
||||
CLASSICAPI_ADVANCED = "Advanced";
|
||||
CLASSICAPI_ALL_SOFT_TARGET_ICONS = "Icons for All Soft Targets";
|
||||
CLASSICAPI_AUTO_DISMOUNT = "Auto Dismount";
|
||||
CLASSICAPI_AUTO_DISMOUNT_TOOLTIP = "Dismount when you cast a spell while you are mounted.";
|
||||
CLASSICAPI_AUTO_DISMOUNT_TOOLTIP = "Dismount when you cast a spell, use an object, loot, attack, or open the flight map while you are mounted.";
|
||||
CLASSICAPI_AUTO_STAND = "Auto Stand";
|
||||
CLASSICAPI_AUTO_STAND_TOOLTIP = "Stand up when you cast a spell while you sit.";
|
||||
CLASSICAPI_AUTO_STAND_TOOLTIP = "Stand up when you cast a spell, loot, or open the flight map while you sit.";
|
||||
CLASSICAPI_AUTO_UNSHIFT = "Auto Cancel Form";
|
||||
CLASSICAPI_AUTO_UNSHIFT_TOOLTIP = "Leave your form when you cast a spell that you cannot cast in it.";
|
||||
CLASSICAPI_AUTO_UNSHIFT_TOOLTIP = "Leave your form when you cast a spell that you cannot cast in it, use an item, or open the flight map. Also lets you talk to NPCs and use objects in druid forms and Ghost Wolf.";
|
||||
CLASSICAPI_BAGS_AND_ITEMS = "Bags and Items";
|
||||
CLASSICAPI_CASTING_AND_ATTACKING = "Casting and Attacking";
|
||||
CLASSICAPI_COMBAT_LOG_RETENTION = "Combat Log Retention";
|
||||
CLASSICAPI_COMBAT_LOG_RETENTION_TOOLTIP = "How many seconds the combat log keeps entries for addons.";
|
||||
CLASSICAPI_CURSOR = "Cursor";
|
||||
CLASSICAPI_DIRECTION_ANY = "Any direction";
|
||||
CLASSICAPI_DIRECTION_CONE = "Within 30 degrees";
|
||||
CLASSICAPI_DIRECTION_FRONT = "Directly in front";
|
||||
@@ -30,6 +31,8 @@ CLASSICAPI_ENEMY_DIRECTION = "Enemy Direction";
|
||||
CLASSICAPI_ENEMY_ICON = "Show Enemy Icon";
|
||||
CLASSICAPI_ENEMY_ICON_TOOLTIP = "Shows an icon over the enemy that action targeting picks.";
|
||||
CLASSICAPI_ENEMY_RANGE = "Enemy Range";
|
||||
CLASSICAPI_EXPANDED_CURSORS = "Show Expanded Cursors";
|
||||
CLASSICAPI_EXPANDED_CURSORS_TOOLTIP = "When you point at an NPC or an object, the cursor shows what you can do with it. For example, an anvil for repairs, ! or ? for quests, and a hook for your fishing bobber.";
|
||||
CLASSICAPI_FRIEND_DIRECTION = "Friend Direction";
|
||||
CLASSICAPI_FRIEND_ICON = "Show Friend Icon";
|
||||
CLASSICAPI_FRIEND_ICON_TOOLTIP = "Shows an icon over the friendly unit that friendly action targeting picks.";
|
||||
@@ -161,6 +164,10 @@ OPTION_TOOLTIP_INTERACT_ICONS = "Controls which icons appear above NPCs and obje
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Stop auto attacking when you switch targets.";
|
||||
KEY_BINDING_NAME_AND_KEY = "%s (%s)";
|
||||
KEY_BINDING_TOOLTIP = "<Right click to unbind>";
|
||||
RESET_POSITION = "Reset Position";
|
||||
ROTATE_LEFT = "Rotate Left";
|
||||
ROTATE_RIGHT = "Rotate Right";
|
||||
ROTATE_TOOLTIP = "Left-click on character and drag to rotate.";
|
||||
SETTINGS_BIND_KEY_TO_COMMAND_OR_CANCEL = "Assign Binding for \"%s\" or Press %s to Cancel";
|
||||
SET_FOCUS = "Set Focus";
|
||||
FOCUS_TOKEN_NOT_FOUND = "<no focus>";
|
||||
|
||||
@@ -8,6 +8,9 @@ BINDING_NAME_FOCUSTARGET = "Establecer objetivo";
|
||||
BINDING_NAME_INTERACTMOUSEOVER = "Interactuar al pasar el ratón";
|
||||
BINDING_NAME_INTERACTTARGET = "Interactuar con objetivo";
|
||||
BINDING_NAME_TARGETFOCUS = "Seleccionar enfocado";
|
||||
CLASSICAPI_CURSOR = "Cursor";
|
||||
CLASSICAPI_EXPANDED_CURSORS = "Mostrar cursores ampliados";
|
||||
CLASSICAPI_EXPANDED_CURSORS_TOOLTIP = "Al pasar el ratón sobre un PNJ o un objeto, el cursor muestra lo que puedes hacer con él. Por ejemplo, un yunque para las reparaciones, ! o ? para las misiones y un anzuelo para tu corcho de pesca.";
|
||||
COMBAT_LABEL = "Combate";
|
||||
COUNTDOWN_FOR_COOLDOWNS_TEXT = "Mostrar tiempos de reutilización en cifras";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "¿Seguro que quieres eliminar el equipamiento %s?";
|
||||
@@ -71,6 +74,10 @@ OPTION_TOOLTIP_INTERACT_ICONS = "Controla los iconos que aparecen sobre los PNJ
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Detiene el ataque automático al cambiar de objetivo.";
|
||||
KEY_BINDING_NAME_AND_KEY = "%s (%s)";
|
||||
KEY_BINDING_TOOLTIP = "<Haz clic derecho para quitar la asignación>";
|
||||
RESET_POSITION = "Restablecer posición";
|
||||
ROTATE_LEFT = "Rotar a la izquierda";
|
||||
ROTATE_RIGHT = "Rotar a la derecha";
|
||||
ROTATE_TOOLTIP = "Haz clic izquierdo en el personaje y arrastra para rotar.";
|
||||
SETTINGS_BIND_KEY_TO_COMMAND_OR_CANCEL = "Asignar atajo para \"%s\" o pulsar %s para cancelar";
|
||||
SET_FOCUS = "Establecer foco";
|
||||
FOCUSTARGET = "Establecer objetivo";
|
||||
|
||||
@@ -8,6 +8,9 @@ BINDING_NAME_FOCUSTARGET = "Définir cible";
|
||||
BINDING_NAME_INTERACTMOUSEOVER = "Interagir par survol de la souris";
|
||||
BINDING_NAME_INTERACTTARGET = "Interagir avec la cible";
|
||||
BINDING_NAME_TARGETFOCUS = "Reprendre cible";
|
||||
CLASSICAPI_CURSOR = "Curseur";
|
||||
CLASSICAPI_EXPANDED_CURSORS = "Afficher les curseurs étendus";
|
||||
CLASSICAPI_EXPANDED_CURSORS_TOOLTIP = "Quand vous survolez un PNJ ou un objet, le curseur indique ce que vous pouvez en faire. Par exemple, une enclume pour les réparations, ! ou ? pour les quêtes et un hameçon pour votre flotteur de pêche.";
|
||||
COMBAT_LABEL = "Combat";
|
||||
COUNTDOWN_FOR_COOLDOWNS_TEXT = "Temps de recharge en chiffres";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "Voulez-vous vraiment supprimer l'ensemble d'équipement %s ?";
|
||||
@@ -71,6 +74,10 @@ OPTION_TOOLTIP_INTERACT_ICONS = "Contrôle les icônes qui s’affichent au-dess
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Interrompt l'attaque automatique lorsque vous changez de cible.";
|
||||
KEY_BINDING_NAME_AND_KEY = "%s (%s)";
|
||||
KEY_BINDING_TOOLTIP = "<Clic droit pour retirer>";
|
||||
RESET_POSITION = "Position par défaut";
|
||||
ROTATE_LEFT = "Pivoter à gauche";
|
||||
ROTATE_RIGHT = "Pivoter à droite";
|
||||
ROTATE_TOOLTIP = "Cliquez sur le personnage et faites glisser la souris pour le faire pivoter.";
|
||||
SETTINGS_BIND_KEY_TO_COMMAND_OR_CANCEL = "Assigner un raccourci pour « %s » ou appuyer sur %s pour annuler";
|
||||
SET_FOCUS = "Définir la focalisation";
|
||||
FOCUSTARGET = "Définir la cible de focalisation";
|
||||
|
||||
@@ -5,6 +5,10 @@ DECIMAL_SEPERATOR = ".";
|
||||
FIRST_NUMBER = "천";
|
||||
FIRST_NUMBER_CAP = "만";
|
||||
FIRST_NUMBER_CAP_NO_SPACE = "만";
|
||||
RESET_POSITION = "위치 초기화";
|
||||
ROTATE_LEFT = "왼쪽으로 회전";
|
||||
ROTATE_RIGHT = "오른쪽으로 회전";
|
||||
ROTATE_TOOLTIP = "마우스 왼쪽 버튼으로 클릭 후 끌어 옮기면 캐릭터를 회전할 수 있습니다.";
|
||||
SECOND_NUMBER = "만";
|
||||
SECOND_NUMBER_CAP = "억";
|
||||
SECOND_NUMBER_CAP_NO_SPACE = "억";
|
||||
@@ -13,3 +17,6 @@ FOURTH_NUMBER = "조";
|
||||
KEY_BINDING_NAME_AND_KEY = "%s (%s)";
|
||||
KEY_BINDING_TOOLTIP = "<지정을 해제하려면 오른쪽 클릭>";
|
||||
SETTINGS_BIND_KEY_TO_COMMAND_OR_CANCEL = "\"%s\" 단축키 지정 또는 %s 눌러 취소";
|
||||
CLASSICAPI_CURSOR = "커서";
|
||||
CLASSICAPI_EXPANDED_CURSORS = "확장 커서 표시";
|
||||
CLASSICAPI_EXPANDED_CURSORS_TOOLTIP = "NPC나 물체를 가리키면 커서가 할 수 있는 행동을 보여줍니다. 예를 들어 수리는 모루, 퀘스트는 ! 또는 ?, 낚시찌는 낚싯바늘로 표시됩니다.";
|
||||
|
||||
@@ -4,14 +4,109 @@ ACTION_BUTTON_USE_KEY_DOWN = "Способности при нажатии кл
|
||||
ACTION_TARGETING_OPTION = "Разрешить динамическое выделение";
|
||||
ADDON_MEM_KB_ABBR = "(%.0f КБ) %s";
|
||||
ADDON_MEM_MB_ABBR = "(%.2f МБ) %s";
|
||||
ADDON_OPTIONS = "Параметры модификаций";
|
||||
BINDING_NAME_AOELOOT = "Сбор добычи по области";
|
||||
BINDING_NAME_FOCUSTARGET = "Запомненная цель";
|
||||
BINDING_NAME_INTERACTMOUSEOVER = "Взаимодействовать указателем мыши";
|
||||
BINDING_NAME_INTERACTMOUSEOVER = "Взаимодействовать с объектом под курсором";
|
||||
BINDING_NAME_INTERACTTARGET = "Взаимодействовать с целью";
|
||||
BINDING_NAME_TARGETFOCUS = "Восстановить цель";
|
||||
CLASSICAPI_ACTION_BARS = "Панели команд";
|
||||
CLASSICAPI_ACTION_TARGETING = "Динамическое выделение";
|
||||
CLASSICAPI_ADVANCED = "Дополнительно";
|
||||
CLASSICAPI_ALL_SOFT_TARGET_ICONS = "Значки для всех мягких целей";
|
||||
CLASSICAPI_AUTO_DISMOUNT = "Автоматически спешиваться";
|
||||
CLASSICAPI_AUTO_DISMOUNT_TOOLTIP = "Спешиваться при применении заклинания, использовании объекта, сборе добычи, атаке или открытии карты полетов, если вы едете верхом.";
|
||||
CLASSICAPI_AUTO_STAND = "Автоматически вставать";
|
||||
CLASSICAPI_AUTO_STAND_TOOLTIP = "Вставать при применении заклинания, сборе добычи или открытии карты полетов, если вы сидите.";
|
||||
CLASSICAPI_AUTO_UNSHIFT = "Автоматически снимать облик";
|
||||
CLASSICAPI_AUTO_UNSHIFT_TOOLTIP = "Покидать свой облик при применении заклинания, которое в нем недоступно, использовании предмета, а также при открытии карты полетов. Также позволяет говорить с NPC и использовать объекты в обликах друида и призрачного волка.";
|
||||
CLASSICAPI_BAGS_AND_ITEMS = "Сумки и предметы";
|
||||
CLASSICAPI_CASTING_AND_ATTACKING = "Заклинания и атаки";
|
||||
CLASSICAPI_COMBAT_LOG_RETENTION = "Хранение журнала боя";
|
||||
CLASSICAPI_COMBAT_LOG_RETENTION_TOOLTIP = "Сколько секунд журнал боя хранит записи для модификаций.";
|
||||
CLASSICAPI_CURSOR = "Курсор";
|
||||
CLASSICAPI_DIRECTION_ANY = "Любое направление";
|
||||
CLASSICAPI_DIRECTION_CONE = "В пределах 30 градусов";
|
||||
CLASSICAPI_DIRECTION_FRONT = "Прямо перед собой";
|
||||
CLASSICAPI_ENEMIES = "Противники";
|
||||
CLASSICAPI_ENEMY_DIRECTION = "Направление на противника";
|
||||
CLASSICAPI_ENEMY_ICON = "Показывать значок противника";
|
||||
CLASSICAPI_ENEMY_ICON_TOOLTIP = "Показывает значок над противником, которого выбирает динамическое выделение.";
|
||||
CLASSICAPI_ENEMY_RANGE = "Дистанция до противников";
|
||||
CLASSICAPI_EXPANDED_CURSORS = "Показывать расширенные курсоры";
|
||||
CLASSICAPI_EXPANDED_CURSORS_TOOLTIP = "При наведении на NPC или объект курсор показывает, что с ним можно сделать. Например, наковальню для ремонта, ! или ? для заданий и крючок для вашего поплавка.";
|
||||
CLASSICAPI_FRIEND_DIRECTION = "Направление на союзника";
|
||||
CLASSICAPI_FRIEND_ICON = "Показывать значок союзника";
|
||||
CLASSICAPI_FRIEND_ICON_TOOLTIP = "Показывает значок над дружественной целью, которую выбирает дружественное динамическое выделение.";
|
||||
CLASSICAPI_FRIEND_RANGE = "Дистанция до союзников";
|
||||
CLASSICAPI_FRIEND_TARGETING = "Включить дружественное динамическое выделение";
|
||||
CLASSICAPI_FRIEND_TARGETING_TOOLTIP = "Выбирает ближайшую дружественную цель в поле зрения. Если у вас нет цели, полезные заклинания применяются к ней.";
|
||||
CLASSICAPI_FRIENDS = "Союзники";
|
||||
CLASSICAPI_GAME_OBJECT_ICON = "Показывать значок взаимодействия на объектах";
|
||||
CLASSICAPI_GAME_OBJECT_ICON_TOOLTIP = "Также показывает значок взаимодействия над объектами, которые нельзя выбрать целью, например почтовыми ящиками и сундуками.";
|
||||
CLASSICAPI_HIDE_EQUIPMENT_SETS = "Скрывать комплекты экипировки на предметах";
|
||||
CLASSICAPI_HIDE_EQUIPMENT_SETS_TOOLTIP = "Убирает строку с комплектами экипировки из подсказок предметов.";
|
||||
CLASSICAPI_ICON_SIZE = "Размер значков";
|
||||
CLASSICAPI_ICON_SIZE_TOOLTIP = "Размер значков взаимодействия, противников и союзников.";
|
||||
CLASSICAPI_IGNORE_BACKPACK = "Не сортировать рюкзак";
|
||||
CLASSICAPI_IGNORE_BACKPACK_TOOLTIP = "При сортировке сумок предметы в рюкзаке остаются на своих местах.";
|
||||
CLASSICAPI_IGNORE_BANK = "Не сортировать основной банк";
|
||||
CLASSICAPI_IGNORE_BANK_TOOLTIP = "При сортировке банка предметы в основных ячейках банка остаются на своих местах.";
|
||||
CLASSICAPI_INTERACT_DIRECTION = "Направление взаимодействия";
|
||||
CLASSICAPI_INTERACT_DIRECTION_TOOLTIP = "Где должен находиться NPC или объект, чтобы его выбрала клавиша взаимодействия. Направление определяется камерой, а не персонажем.";
|
||||
CLASSICAPI_INTERACT_ICON = "Показывать значок взаимодействия";
|
||||
CLASSICAPI_INTERACT_ICON_TOOLTIP = "Показывает значок над NPC или объектом, которые использует клавиша взаимодействия.";
|
||||
CLASSICAPI_INTERACT_KEY = "Клавиша взаимодействия";
|
||||
CLASSICAPI_INTERACT_RANGE = "Дистанция взаимодействия";
|
||||
CLASSICAPI_INTERFACE = "Интерфейс";
|
||||
CLASSICAPI_KEEP_ACTION_TARGETING = "Сохранять динамическое выделение";
|
||||
CLASSICAPI_KEEP_ACTION_TARGETING_TOOLTIP = "Динамическое выделение продолжает выбирать противников и союзников, пока у вас есть цель, выбранная вручную.";
|
||||
CLASSICAPI_LOOSE_TARGET_FOLLOWS = "За кем следует обычная цель";
|
||||
CLASSICAPI_LOOSE_TARGET_FOLLOWS_ENEMY = "За мягкой целью противника";
|
||||
CLASSICAPI_LOOSE_TARGET_FOLLOWS_FRIEND = "За мягкой целью союзника";
|
||||
CLASSICAPI_LOOSE_TARGET_FOLLOWS_NONE = "Ни за кем";
|
||||
CLASSICAPI_LOOSE_TARGET_FOLLOWS_TOOLTIP = "Цель, выбранная динамическим выделением, меняется вместе с мягкой целью противника или союзника. Цель, выбранная вручную, сохраняется.";
|
||||
CLASSICAPI_LOW_PRIORITY_ICONS = "Показывать значки низкого приоритета";
|
||||
CLASSICAPI_LOW_PRIORITY_ICONS_TOOLTIP = "Также показывает значок над целью, с которой нельзя ничего сделать, например над членом группы. Значок выглядит как обычный указатель.";
|
||||
CLASSICAPI_MATCH_INSIDE_NAMES = "Учитывать правки внутри имени";
|
||||
CLASSICAPI_MATCH_INSIDE_NAMES_TOOLTIP = "Когда вы вводите текст в середине имени, предлагаемые имена также учитывают буквы после курсора.";
|
||||
CLASSICAPI_MATCH_THE_COMMAND = "Учитывать команду";
|
||||
CLASSICAPI_MATCH_THE_COMMAND_TOOLTIP = "Предлагает только имена, подходящие для команды, например участников гильдии для /gpromote. Если выключено, предлагаются все известные вам имена.";
|
||||
CLASSICAPI_MOTION = "Движение";
|
||||
CLASSICAPI_NAME_SUGGESTIONS = "Подсказки имен";
|
||||
CLASSICAPI_NAMEPLATES = "Таблички с именами";
|
||||
CLASSICAPI_NUMBERS = "Числа";
|
||||
CLASSICAPI_OPTIONS_ABOUT = "Настройки возможностей, которые ClassicAPI добавляет в игру. Каждый параметр меняет одну настройку, и изменение применяется сразу.";
|
||||
CLASSICAPI_OTHER_NAMEPLATE_ALPHA = "Непрозрачность чужих табличек";
|
||||
CLASSICAPI_OTHER_NAMEPLATE_ALPHA_TOOLTIP = "Непрозрачность табличек с именами целей, которые не являются вашей целью.";
|
||||
CLASSICAPI_PREFER_YOUR_TARGET = "Отдавать предпочтение вашей цели";
|
||||
CLASSICAPI_PREFER_YOUR_TARGET_TOOLTIP = "Если выбранную вами цель можно взять, динамическое выделение выбирает ее. Если параметр выключен, всегда выбирается ближайшая цель.";
|
||||
CLASSICAPI_RANGE_TOOLTIP = "Максимальное расстояние в ярдах. Оно не может превышать значение параметра targetNearestDistance.";
|
||||
CLASSICAPI_RECENT_WHISPERS_FIRST = "Сначала недавние шепоты";
|
||||
CLASSICAPI_RECENT_WHISPERS_FIRST_TOOLTIP = "Предлагаемые имена начинаются с игроков, которым вы шептали совсем недавно.";
|
||||
CLASSICAPI_REMEMBER_PLAYERS = "Запоминать игроков";
|
||||
CLASSICAPI_REMEMBER_PLAYERS_TOOLTIP = "Сохраняет имя, класс, расу и пол встречавшихся вам игроков, чтобы модификации могли показывать их в следующих сеансах. Данные общие для всех ваших персонажей на этом сервере.";
|
||||
CLASSICAPI_SCRIPT_PROFILE = "Профилирование скриптов";
|
||||
CLASSICAPI_SCRIPT_PROFILE_TOOLTIP = "Измеряет время процессора, которое используют модификации. Изменение вступает в силу после перезагрузки интерфейса.";
|
||||
CLASSICAPI_SEPARATE_LARGE_NUMBERS = "Разделять большие числа";
|
||||
CLASSICAPI_SEPARATE_LARGE_NUMBERS_TOOLTIP = "Ставит разделитель между группами из трех цифр, когда модификации форматируют числа с помощью BreakUpLargeNumbers.";
|
||||
CLASSICAPI_SORT_BAGS_RIGHT_TO_LEFT = "Сортировать сумки справа налево";
|
||||
CLASSICAPI_SORT_BAGS_RIGHT_TO_LEFT_TOOLTIP = "При сортировке предметы перемещаются в конец последней сумки, а хлам — в начало рюкзака.";
|
||||
CLASSICAPI_STRICT_INTERACT_RANGE = "Строгая дистанция взаимодействия";
|
||||
CLASSICAPI_STRICT_INTERACT_RANGE_TOOLTIP = "Никогда не выбирает NPC или объект за пределами дистанции взаимодействия. Если параметр выключен, объект, доступный для использования сейчас, выбирается даже на большем расстоянии.";
|
||||
CLASSICAPI_TARGET_NAMEPLATE_ALPHA = "Непрозрачность таблички цели";
|
||||
CLASSICAPI_TARGET_NAMEPLATE_ALPHA_TOOLTIP = "Непрозрачность таблички с именем вашей цели.";
|
||||
CLASSICAPI_UI_SHAKE_STRENGTH = "Сила дрожания интерфейса";
|
||||
CLASSICAPI_UI_SHAKE_STRENGTH_TOOLTIP = "Определяет, насколько сильно эффекты дрожания смещают рамки. 0 отключает дрожание.";
|
||||
CLASSICAPI_UNIT_DIRECTION_TOOLTIP = "Где должна находиться цель, чтобы ее выбрало динамическое выделение. Направление определяется камерой, а не персонажем.";
|
||||
CLASSICAPI_WHEN_YOU_HAVE_A_TARGET = "Когда у вас есть цель";
|
||||
COMBAT_LABEL = "Бой";
|
||||
COUNTDOWN_FOR_COOLDOWNS_TEXT = "Отсчет времени восстановления";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "Вы действительно хотите удалить комплект экипировки \"%s\"?";
|
||||
CONFIRM_SAVE_EQUIPMENT_SET = "Хотите сохранить комплект экипировки \"%s\"?";
|
||||
COPPER_AMOUNT = "%d |4медная монета:медные монеты:медных монет;";
|
||||
COPPER_AMOUNT_SYMBOL = "м";
|
||||
COPPER_AMOUNT_TEXTURE = "%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:2:0:4:1:2:3:0:1|t";
|
||||
DEBUFF_SYMBOL_CURSE = "Пр";
|
||||
DEBUFF_SYMBOL_DISEASE = "Бз";
|
||||
DEBUFF_SYMBOL_MAGIC = "Мг";
|
||||
@@ -21,74 +116,70 @@ ENABLE_INTERACT_TEXT = "Включить клавишу взаимодейств
|
||||
EQUIPMENT_MANAGER = "Комплекты экипировки";
|
||||
EQUIPMENT_MANAGER_IGNORE_SLOT = "Пропустить ячейку";
|
||||
EQUIPMENT_MANAGER_IS_DISABLED = "Управление экипировкой отключено.";
|
||||
EQUIPMENT_MANAGER_ITEMS_MISSING_TOOLTIP = "%d %s |cffff0000(предмет для ячейки \"%d\" отсутствует)|r";
|
||||
EQUIPMENT_MANAGER_ITEMS_MISSING_TOOLTIP = "%d %s |cffff0000(не хватает: %d)|r";
|
||||
EQUIPMENT_MANAGER_PLACE_IN_BAGS = "Поместить в сумку";
|
||||
EQUIPMENT_MANAGER_UNIGNORE_SLOT = "Учитывать ячейку";
|
||||
EQUIPMENT_SETS = "Комплекты экипировки: |cFFFFFFFF%s|r";
|
||||
EQUIPMENT_SETS_CANT_RENAME = "Это название уже используется для другого комплекта.";
|
||||
EQUIPMENT_SETS_TOO_MANY = "Вы не можете создавать новые комплекты экипировки.";
|
||||
EQUIPMENT_SET_ASSIGN_TO_SPEC = "Выбрать специализацию:";
|
||||
EQUIPMENT_SET_ASSIGN_TO_SPEC = "Назначить специализации:";
|
||||
EQUIPMENT_SET_EDIT = "Изменить название/значок";
|
||||
EQUIPMENT_SET_SETTINGS = "Параметры";
|
||||
EQUIPSET_EQUIP = "Надеть";
|
||||
ERR_EQUIPMENT_MANAGER_BAGS_FULL = "Не удалось заменить комплект обмундирования. Инвентарь переполнен.";
|
||||
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "Некоторые предметы из набора \"%s\" не будут экипированы, поскольку вы участвуете в бою.";
|
||||
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "Недостает одного или более предметов из набора \"%s\".";
|
||||
ERR_EQUIPMENT_MANAGER_BAGS_FULL = "Не удалось сменить экипировку. Инвентарь переполнен.";
|
||||
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "Некоторые предметы из комплекта \"%s\" нельзя сменить, поскольку вы участвуете в бою.";
|
||||
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "Один или несколько предметов из комплекта \"%s\" отсутствуют и не были надеты.";
|
||||
EVENTTRACE_APPLY_DEFAULT_FILTER = "Применить фильтр по умолчанию";
|
||||
EVENTTRACE_ARG_FMT = "Арг. %d:";
|
||||
EVENTTRACE_BUTTON_DISABLE_FILTERS = "Снять выделение";
|
||||
EVENTTRACE_ARG_FMT = "Аргумент %d:";
|
||||
EVENTTRACE_BUTTON_DISABLE_FILTERS = "Снять все";
|
||||
EVENTTRACE_BUTTON_DISCARD_FILTER = "Сбросить все";
|
||||
EVENTTRACE_BUTTON_ENABLE_FILTERS = "Выделить все";
|
||||
EVENTTRACE_BUTTON_ENABLE_FILTERS = "Выбрать все";
|
||||
EVENTTRACE_BUTTON_MARKER = "Маркер";
|
||||
EVENTTRACE_BUTTON_PAUSE = "Пауза";
|
||||
EVENTTRACE_BUTTON_PLAY = "Играть";
|
||||
EVENTTRACE_BUTTON_PLAY = "Продолжить";
|
||||
EVENTTRACE_FILTER_HEADER = "Фильтр";
|
||||
EVENTTRACE_HEADER = "Журнал события";
|
||||
EVENTTRACE_LOG_CR_EVENTS = "Отображать глобальные события по регистрации";
|
||||
EVENTTRACE_HEADER = "Журнал событий";
|
||||
EVENTTRACE_LOG_CR_EVENTS = "Показывать события глобального реестра";
|
||||
EVENTTRACE_LOG_DISCARD = "Журнал сброшен";
|
||||
EVENTTRACE_LOG_HEADER = "Журнал";
|
||||
EVENTTRACE_LOG_PAUSE = "Регистрация участников события приостановлена";
|
||||
EVENTTRACE_LOG_PAUSE_WHILE_HIDDEN = "Фиксация событий прекращается в свернутом состоянии";
|
||||
EVENTTRACE_LOG_START = "Регистрация участников события началась";
|
||||
EVENTTRACE_LOG_WHEN_HIDDEN = "Фиксировать события в свернутом состоянии";
|
||||
EVENTTRACE_LOG_PAUSE = "Запись событий приостановлена";
|
||||
EVENTTRACE_LOG_PAUSE_WHILE_HIDDEN = "Запись событий приостановлена, пока окно скрыто";
|
||||
EVENTTRACE_LOG_START = "Запись событий начата";
|
||||
EVENTTRACE_LOG_WHEN_HIDDEN = "Записывать события, когда окно скрыто";
|
||||
EVENTTRACE_MARKER = "Маркер";
|
||||
EVENTTRACE_MESSAGE_FORMAT = "--- %s ---";
|
||||
EVENTTRACE_OPTIONS = "Параметры";
|
||||
EVENTTRACE_RESULTS = "Результаты: %d";
|
||||
EVENTTRACE_SHOW_ARGUMENTS = "Отображать аргументы события";
|
||||
EVENTTRACE_SHOW_TIMESTAMP = "Отображать метку времени события";
|
||||
EVENTTRACE_RESULTS = "Результатов: %d";
|
||||
EVENTTRACE_SHOW_ARGUMENTS = "Показывать аргументы событий";
|
||||
EVENTTRACE_SHOW_TIMESTAMP = "Показывать время событий";
|
||||
EVENTTRACE_TIMESTAMP = "Время:";
|
||||
CLEAR_FOCUS = "Отменить запоминание цели";
|
||||
INTERACT_ICONS_DEFAULT = "Только NPC (по умолчанию)";
|
||||
INTERACT_ICONS_OPTION = "Значки клавиш взаимодействия";
|
||||
INTERACT_ICONS_SHOW_ALL = "Показать все";
|
||||
INTERACT_ICONS_OPTION = "Значки клавиши взаимодействия";
|
||||
INTERACT_ICONS_SHOW_ALL = "Показывать все";
|
||||
INTERACT_ICONS_SHOW_NONE = "Не показывать";
|
||||
OPTION_TOOLTIP_ACTION_BUTTON_USE_KEY_DOWN = "Способности будут задействоваться, когда пользователь нажимает, а не отпускает клавишу.";
|
||||
OPTION_TOOLTIP_ACTION_TARGETING = "Система захвата целей, динамически выбирающая цели в поле зрения. Работает параллельно со стандартной системой наведения.";
|
||||
OPTION_TOOLTIP_COUNTDOWN_FOR_COOLDOWNS = "Отображение обратного отсчета для времени восстановления на панели команд.";
|
||||
OPTION_TOOLTIP_ENABLE_INTERACT = "Позволяет взаимодействовать с NPC и объектами по нажатию клавиши.";
|
||||
OPTION_TOOLTIP_INTERACT_ICONS = "Определяет, какие значки появляются над NPC и объектами, если включен параметр клавиши взаимодействия.";
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Прекращение автоматической атаки при смене цели.";
|
||||
OPTION_TOOLTIP_ACTION_BUTTON_USE_KEY_DOWN = "Способности на панели команд срабатывают при нажатии клавиши, а не при ее отпускании.";
|
||||
OPTION_TOOLTIP_ACTION_TARGETING = "Система выделения, которая динамически выбирает противников в зависимости от направления взгляда. Работает вместе со стандартной системой выделения.";
|
||||
OPTION_TOOLTIP_COUNTDOWN_FOR_COOLDOWNS = "Показывать числовой отсчет времени восстановления на панели команд.";
|
||||
OPTION_TOOLTIP_ENABLE_INTERACT = "Позволяет взаимодействовать с NPC и объектами нажатием клавиши.";
|
||||
OPTION_TOOLTIP_INTERACT_ICONS = "Определяет, какие значки появляются над NPC и объектами, когда включена клавиша взаимодействия.";
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Прекращать автоатаку при смене цели.";
|
||||
KEY_BINDING_NAME_AND_KEY = "%s (%s)";
|
||||
KEY_BINDING_TOOLTIP = "<ПКМ, чтобы отвязать>";
|
||||
SETTINGS_BIND_KEY_TO_COMMAND_OR_CANCEL = "Назначьте клавишу \"%s\"или нажмите %s для отмены";
|
||||
KEY_BINDING_TOOLTIP = "<ПКМ, чтобы снять привязку>";
|
||||
RESET_POSITION = "Сброс позиции";
|
||||
ROTATE_LEFT = "Вращение влево";
|
||||
ROTATE_RIGHT = "Вращение вправо";
|
||||
ROTATE_TOOLTIP = "Зажмите ЛКМ и тащите курсор, чтобы вращать изображение.";
|
||||
SETTINGS_BIND_KEY_TO_COMMAND_OR_CANCEL = "Назначьте клавишу для \"%s\" или нажмите %s для отмены";
|
||||
SET_FOCUS = "Запомнить цель";
|
||||
FOCUS_TOKEN_NOT_FOUND = "<фокус отсутствует>";
|
||||
FOCUSTARGET = "Запомнить цель";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d |4предмет:предмета:предметов;";
|
||||
ITEMS_EQUIPPED = "%d |4предмет экипирован:предмета экипировано:предметов экипировано;";
|
||||
ITEMS_IN_INVENTORY = "%d |4предмет:предмета:предметов; в сумках";
|
||||
ITEM_SLOTS_IGNORED = "%d |4ячейка пропущена:ячейки пропущены:ячейки пропущены;";
|
||||
ITEM_MISSING = "Предмет для ячейки %s отсутствует";
|
||||
ITEMS_NOT_IN_INVENTORY = "%d |4предмет отсутствует:предмета отсутствует:предметов отсутствуют;";
|
||||
STOP_AUTO_ATTACK = "Прекращение автоатаки";
|
||||
TARGETFOCUS = "Восстановить цель";
|
||||
-- BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
COPPER_AMOUNT = "%d |4медная монета:медные монеты:медных монет;";
|
||||
COPPER_AMOUNT_SYMBOL = "м";
|
||||
GEARSETS_POPUP_TEXT = "Введите название комплекта (не более 16 символов):";
|
||||
GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT = "%d |4золотая:золотые:золотых;";
|
||||
GOLD_AMOUNT_SYMBOL = "з";
|
||||
GOLD_AMOUNT_TEXTURE = "%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:2:0:4:1:0:1:0:1|t";
|
||||
GOLD_AMOUNT_TEXTURE_STRING = "%s|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:2:0:4:1:0:1:0:1|t";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
ICON_FILTER_ALL = "Все значки";
|
||||
ICON_FILTER_ITEM = "Предметы";
|
||||
@@ -106,17 +197,17 @@ ITEM_MOD_ATTACK_POWER_SHORT = "к силе атаки";
|
||||
ITEM_MOD_RANGED_ATTACK_POWER_SHORT = "к силе атаки дальнего боя";
|
||||
ITEM_MOD_CRIT_MELEE_RATING_SHORT = "к показателю критического удара в ближнем бою";
|
||||
ITEM_MOD_CRIT_RANGED_RATING_SHORT = "к показателю критического удара в дальнем бою";
|
||||
ITEM_MOD_HIT_MELEE_RATING_SHORT = "к меткости в ближнем бою";
|
||||
ITEM_MOD_HIT_MELEE_RATING_SHORT = "к меткости в ближнем бою";
|
||||
ITEM_MOD_HIT_RANGED_RATING_SHORT = "к меткости в дальнем бою";
|
||||
ITEM_MOD_HIT_SPELL_RATING_SHORT = "к меткости заклинаний";
|
||||
ITEM_MOD_CRIT_SPELL_RATING_SHORT = "к критическому эффекту заклинаний";
|
||||
ITEM_MOD_SPELL_DAMAGE_DONE_SHORT = "к дополнительному урону";
|
||||
ITEM_MOD_SPELL_HEALING_DONE_SHORT = "к дополнительному исцелению";
|
||||
ITEM_MOD_MANA_REGENERATION_SHORT = "к восполнению маны";
|
||||
ITEM_MOD_DEFENSE_SKILL_RATING_SHORT = "Защита";
|
||||
ITEM_MOD_DODGE_RATING_SHORT = "Уклонение";
|
||||
ITEM_MOD_PARRY_RATING_SHORT = "Парирование";
|
||||
ITEM_MOD_BLOCK_RATING_SHORT = "Блок";
|
||||
ITEM_MOD_DEFENSE_SKILL_RATING_SHORT = "к защите";
|
||||
ITEM_MOD_DODGE_RATING_SHORT = "к уклонению";
|
||||
ITEM_MOD_PARRY_RATING_SHORT = "к парированию";
|
||||
ITEM_MOD_BLOCK_RATING_SHORT = "к блокированию";
|
||||
ITEM_MOD_BLOCK_VALUE_SHORT = "к показателю блокирования";
|
||||
ITEM_MOD_HOLY_SPELL_DAMAGE_DONE_SHORT = "к урону от магии света";
|
||||
ITEM_MOD_FIRE_SPELL_DAMAGE_DONE_SHORT = "к урону от магии огня";
|
||||
@@ -128,6 +219,12 @@ ITEM_MOD_SPELL_PENETRATION_SHORT = "к проникающей способнос
|
||||
ITEM_MOD_ARMOR_PENETRATION_RATING_SHORT = "к пробиванию брони";
|
||||
ITEM_MOD_HEALTH_REGEN_SHORT = "к восполнению здоровья";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "к урону в секунду";
|
||||
ITEMS_EQUIPPED = "%d |4предмет экипирован:предмета экипировано:предметов экипировано;";
|
||||
ITEMS_IN_INVENTORY = "%d |4предмет:предмета:предметов; в инвентаре";
|
||||
ITEMS_NOT_IN_INVENTORY = "%d |4предмет отсутствует:предмета отсутствует:предметов отсутствует;";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d |4предмет:предмета:предметов;";
|
||||
ITEM_MISSING = "%s отсутствует";
|
||||
ITEM_SLOTS_IGNORED = "%d |4ячейка пропущена:ячейки пропущены:ячеек пропущено;";
|
||||
LARGE_NUMBER_SEPERATOR = "";
|
||||
DECIMAL_SEPERATOR = ",";
|
||||
FIRST_NUMBER = "тыс.";
|
||||
@@ -138,14 +235,18 @@ SECOND_NUMBER_CAP = "М";
|
||||
SECOND_NUMBER_CAP_NO_SPACE = "М";
|
||||
THIRD_NUMBER = "млрд";
|
||||
FOURTH_NUMBER = "трлн";
|
||||
LUA_ERROR = "Ошибка Lua";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PAPERDOLL_EQUIPMENTMANAGER = "Комплекты экипировки";
|
||||
PAPERDOLL_NEWEQUIPMENTSET = "Новый комплект";
|
||||
PERCENTAGE_STRING = "%d%%";
|
||||
SET_FRIENDNOTE_LABEL = "Заметка о |3-5(%s):";
|
||||
PLAYER_LIST_DELIMITER = ", ";
|
||||
RELOADUI = "Перезагрузить интерфейс";
|
||||
SET_FRIENDNOTE_LABEL = "Задать заметку для %s:";
|
||||
SET_NOTE = "Добавить заметку";
|
||||
SILVER_AMOUNT = "%d |4серебряная:серебряные:серебряных;";
|
||||
SILVER_AMOUNT_SYMBOL = "с";
|
||||
SILVER_AMOUNT_TEXTURE = "%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:2:0:4:1:1:2:0:1|t";
|
||||
SLASH_CANCELAURA1 = "/снятьауру";
|
||||
SLASH_CANCELAURA2 = "/cancelaura";
|
||||
SLASH_CANCELFORM1 = "/нормоблик";
|
||||
@@ -219,10 +320,14 @@ SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
|
||||
SLASH_USE1 = "/use";
|
||||
SLASH_USE2 = "/использовать";
|
||||
SOCIAL_LABEL = "Общение";
|
||||
LINK_TRADESKILL_TOOLTIP = "Щелкните здесь, чтобы создать ссылку на вашу профессию.";
|
||||
TEXT_TO_SPEECH = "Текст в речь";
|
||||
TEXT_TO_SPEECH_ADJUST_RATE = "Изменить скорость речи.";
|
||||
STOP_AUTO_ATTACK = "Прекращение автоатаки";
|
||||
SYMBOLIC_NEGATIVE_NUMBER = "-%s";
|
||||
SYMBOLIC_POSITIVE_NUMBER = "+%s";
|
||||
TARGETFOCUS = "Восстановить цель";
|
||||
LINK_TRADESKILL_TOOLTIP = "Щелкните здесь, чтобы создать ссылку на свою профессию.";
|
||||
TEXT_TO_SPEECH = "Синтез речи";
|
||||
TEXT_TO_SPEECH_ADJUST_RATE = "Изменить скорость речи";
|
||||
TEXT_TO_SPEECH_ADJUST_VOLUME = "Громкость речи";
|
||||
TEXT_TO_SPEECH_VOICE_OPTION = "Параметры голоса";
|
||||
TOTAL_MEM_KB_ABBR = "Память: %.0f КБ";
|
||||
TOTAL_MEM_MB_ABBR = "Модификации занимают в памяти: %.2f МБ";
|
||||
TEXT_TO_SPEECH_VOICE_OPTION = "Выбор голоса";
|
||||
TOTAL_MEM_KB_ABBR = "Память модификаций: %.0f КБ";
|
||||
TOTAL_MEM_MB_ABBR = "Память модификаций: %.2f МБ";
|
||||
|
||||
@@ -4,10 +4,102 @@ ACTION_BUTTON_USE_KEY_DOWN = "按下快捷键时施法";
|
||||
ACTION_TARGETING_OPTION = "开启动作瞄准";
|
||||
ADDON_MEM_KB_ABBR = "(%.0f KB) %s";
|
||||
ADDON_MEM_MB_ABBR = "(%.2f MB) %s";
|
||||
ADDON_OPTIONS = "插件设置";
|
||||
BINDING_NAME_AOELOOT = "范围拾取";
|
||||
BINDING_NAME_FOCUSTARGET = "焦点目标";
|
||||
BINDING_NAME_INTERACTMOUSEOVER = "与鼠标悬停处互动";
|
||||
BINDING_NAME_INTERACTTARGET = "与目标互动";
|
||||
BINDING_NAME_TARGETFOCUS = "目标焦点";
|
||||
CLASSICAPI_ACTION_BARS = "动作条";
|
||||
CLASSICAPI_ACTION_TARGETING = "动作瞄准";
|
||||
CLASSICAPI_ADVANCED = "高级";
|
||||
CLASSICAPI_ALL_SOFT_TARGET_ICONS = "所有软目标图标";
|
||||
CLASSICAPI_AUTO_DISMOUNT = "自动下马";
|
||||
CLASSICAPI_AUTO_DISMOUNT_TOOLTIP = "骑乘状态下施放法术、使用物体、拾取、攻击或打开飞行地图时自动下马。";
|
||||
CLASSICAPI_AUTO_STAND = "自动站起";
|
||||
CLASSICAPI_AUTO_STAND_TOOLTIP = "坐下状态下施放法术、拾取或打开飞行地图时自动站起。";
|
||||
CLASSICAPI_AUTO_UNSHIFT = "自动取消形态";
|
||||
CLASSICAPI_AUTO_UNSHIFT_TOOLTIP = "施放当前形态无法施放的法术、使用物品或打开飞行地图时自动取消该形态。同时允许在德鲁伊形态和幽魂之狼形态下与NPC对话和使用物体。";
|
||||
CLASSICAPI_BAGS_AND_ITEMS = "背包与物品";
|
||||
CLASSICAPI_CASTING_AND_ATTACKING = "施法与攻击";
|
||||
CLASSICAPI_COMBAT_LOG_RETENTION = "战斗记录保留时间";
|
||||
CLASSICAPI_COMBAT_LOG_RETENTION_TOOLTIP = "战斗记录为插件保留条目的秒数。";
|
||||
CLASSICAPI_CURSOR = "鼠标指针";
|
||||
CLASSICAPI_DIRECTION_ANY = "任意方向";
|
||||
CLASSICAPI_DIRECTION_CONE = "30度范围内";
|
||||
CLASSICAPI_DIRECTION_FRONT = "正前方";
|
||||
CLASSICAPI_ENEMIES = "敌人";
|
||||
CLASSICAPI_ENEMY_DIRECTION = "敌人方向";
|
||||
CLASSICAPI_ENEMY_ICON = "显示敌人图标";
|
||||
CLASSICAPI_ENEMY_ICON_TOOLTIP = "在动作瞄准选中的敌人上方显示图标。";
|
||||
CLASSICAPI_ENEMY_RANGE = "敌人范围";
|
||||
CLASSICAPI_EXPANDED_CURSORS = "显示扩展鼠标指针";
|
||||
CLASSICAPI_EXPANDED_CURSORS_TOOLTIP = "指向NPC或物体时,鼠标指针会显示你可以对其进行的操作。例如,铁砧表示修理,! 或 ? 表示任务,鱼钩表示你的鱼漂。";
|
||||
CLASSICAPI_FRIEND_DIRECTION = "友方方向";
|
||||
CLASSICAPI_FRIEND_ICON = "显示友方图标";
|
||||
CLASSICAPI_FRIEND_ICON_TOOLTIP = "在友方动作瞄准选中的友方单位上方显示图标。";
|
||||
CLASSICAPI_FRIEND_RANGE = "友方范围";
|
||||
CLASSICAPI_FRIEND_TARGETING = "开启友方动作瞄准";
|
||||
CLASSICAPI_FRIEND_TARGETING_TOOLTIP = "选中你视野方向最近的友方单位。当你没有目标时,你的有益法术会施放给它。";
|
||||
CLASSICAPI_FRIENDS = "友方";
|
||||
CLASSICAPI_GAME_OBJECT_ICON = "在物体上显示交互图标";
|
||||
CLASSICAPI_GAME_OBJECT_ICON_TOOLTIP = "同时在你无法选中的物体上方显示交互图标,例如邮箱和箱子。";
|
||||
CLASSICAPI_HIDE_EQUIPMENT_SETS = "在物品上隐藏装备方案";
|
||||
CLASSICAPI_HIDE_EQUIPMENT_SETS_TOOLTIP = "从物品提示信息中移除装备方案一行。";
|
||||
CLASSICAPI_ICON_SIZE = "图标大小";
|
||||
CLASSICAPI_ICON_SIZE_TOOLTIP = "交互、敌人和友方图标的大小。";
|
||||
CLASSICAPI_IGNORE_BACKPACK = "整理时忽略行囊";
|
||||
CLASSICAPI_IGNORE_BACKPACK_TOOLTIP = "整理背包时,行囊中的物品保持原位。";
|
||||
CLASSICAPI_IGNORE_BANK = "整理时忽略主银行";
|
||||
CLASSICAPI_IGNORE_BANK_TOOLTIP = "整理银行时,主银行栏位中的物品保持原位。";
|
||||
CLASSICAPI_INTERACT_DIRECTION = "交互方向";
|
||||
CLASSICAPI_INTERACT_DIRECTION_TOOLTIP = "NPC或物体需要处于什么方向,交互按键才会选中它。方向以镜头为准,而不是以角色为准。";
|
||||
CLASSICAPI_INTERACT_ICON = "显示交互图标";
|
||||
CLASSICAPI_INTERACT_ICON_TOOLTIP = "在交互按键选中的NPC或物体上方显示图标。";
|
||||
CLASSICAPI_INTERACT_KEY = "交互按键";
|
||||
CLASSICAPI_INTERACT_RANGE = "交互范围";
|
||||
CLASSICAPI_INTERFACE = "界面";
|
||||
CLASSICAPI_KEEP_ACTION_TARGETING = "保持动作瞄准";
|
||||
CLASSICAPI_KEEP_ACTION_TARGETING_TOOLTIP = "当你选定了自己选择的目标时,动作瞄准仍会选中敌人和友方。";
|
||||
CLASSICAPI_LOOSE_TARGET_FOLLOWS = "自由目标跟随";
|
||||
CLASSICAPI_LOOSE_TARGET_FOLLOWS_ENEMY = "软目标敌人";
|
||||
CLASSICAPI_LOOSE_TARGET_FOLLOWS_FRIEND = "软目标友方";
|
||||
CLASSICAPI_LOOSE_TARGET_FOLLOWS_NONE = "不跟随";
|
||||
CLASSICAPI_LOOSE_TARGET_FOLLOWS_TOOLTIP = "动作瞄准设置的目标会跟随软目标敌人或软目标友方变化。你自己选择的目标则保持不变。";
|
||||
CLASSICAPI_LOW_PRIORITY_ICONS = "显示低优先级图标";
|
||||
CLASSICAPI_LOW_PRIORITY_ICONS_TOOLTIP = "同时在你无法对其做任何操作的单位上方显示图标,例如队伍成员。该图标为普通指针。";
|
||||
CLASSICAPI_MATCH_INSIDE_NAMES = "匹配名称中间的编辑";
|
||||
CLASSICAPI_MATCH_INSIDE_NAMES_TOOLTIP = "当你在名称中间输入时,建议的名称也会匹配光标之后的字母。";
|
||||
CLASSICAPI_MATCH_THE_COMMAND = "匹配命令";
|
||||
CLASSICAPI_MATCH_THE_COMMAND_TOOLTIP = "只建议符合命令的名称,例如 /gpromote 时只建议公会成员。关闭时,会建议你认识的所有名称。";
|
||||
CLASSICAPI_MOTION = "动态效果";
|
||||
CLASSICAPI_NAME_SUGGESTIONS = "名称建议";
|
||||
CLASSICAPI_NAMEPLATES = "姓名板";
|
||||
CLASSICAPI_NUMBERS = "数字";
|
||||
CLASSICAPI_OPTIONS_ABOUT = "ClassicAPI为游戏添加的功能设置。每个选项对应一项设置,修改后立即生效。";
|
||||
CLASSICAPI_OTHER_NAMEPLATE_ALPHA = "其他姓名板不透明度";
|
||||
CLASSICAPI_OTHER_NAMEPLATE_ALPHA_TOOLTIP = "非目标单位姓名板的不透明度。";
|
||||
CLASSICAPI_PREFER_YOUR_TARGET = "优先你的目标";
|
||||
CLASSICAPI_PREFER_YOUR_TARGET_TOOLTIP = "当你选择的目标可以被选中时,动作瞄准会优先选中它。关闭时,总是选中最近的单位。";
|
||||
CLASSICAPI_RANGE_TOOLTIP = "最大距离(码)。不能大于targetNearestDistance设置。";
|
||||
CLASSICAPI_RECENT_WHISPERS_FIRST = "优先最近的密语";
|
||||
CLASSICAPI_RECENT_WHISPERS_FIRST_TOOLTIP = "名称建议从你最近密语过的玩家开始。";
|
||||
CLASSICAPI_REMEMBER_PLAYERS = "记住玩家";
|
||||
CLASSICAPI_REMEMBER_PLAYERS_TOOLTIP = "保存你遇到的玩家的名字、职业、种族和性别,以便插件在以后的游戏中显示。你在该服务器的所有角色共享这些数据。";
|
||||
CLASSICAPI_SCRIPT_PROFILE = "脚本性能分析";
|
||||
CLASSICAPI_SCRIPT_PROFILE_TOOLTIP = "统计插件占用的CPU时间。更改将在你重载界面后生效。";
|
||||
CLASSICAPI_SEPARATE_LARGE_NUMBERS = "分隔大数字";
|
||||
CLASSICAPI_SEPARATE_LARGE_NUMBERS_TOOLTIP = "当插件使用BreakUpLargeNumbers格式化数字时,每三位数字之间添加分隔符,例如1,234,567。";
|
||||
CLASSICAPI_SORT_BAGS_RIGHT_TO_LEFT = "从右到左整理背包";
|
||||
CLASSICAPI_SORT_BAGS_RIGHT_TO_LEFT_TOOLTIP = "整理时,将你的物品放到最后一个背包的末尾,将垃圾放到行囊的开头。";
|
||||
CLASSICAPI_STRICT_INTERACT_RANGE = "严格交互范围";
|
||||
CLASSICAPI_STRICT_INTERACT_RANGE_TOOLTIP = "绝不选中超出交互范围的NPC或物体。关闭时,即使超出范围,也会选中当前可以交互的目标。";
|
||||
CLASSICAPI_TARGET_NAMEPLATE_ALPHA = "目标姓名板不透明度";
|
||||
CLASSICAPI_TARGET_NAMEPLATE_ALPHA_TOOLTIP = "你目标姓名板的不透明度。";
|
||||
CLASSICAPI_UI_SHAKE_STRENGTH = "界面震动强度";
|
||||
CLASSICAPI_UI_SHAKE_STRENGTH_TOOLTIP = "缩放震动效果移动框架的距离。0表示关闭震动。";
|
||||
CLASSICAPI_UNIT_DIRECTION_TOOLTIP = "单位需要处于什么方向,动作瞄准才会选中它。方向以镜头为准,而不是以角色为准。";
|
||||
CLASSICAPI_WHEN_YOU_HAVE_A_TARGET = "当你拥有目标时";
|
||||
COMBAT_LABEL = "战斗";
|
||||
COUNTDOWN_FOR_COOLDOWNS_TEXT = "显示冷却时间";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "你确认要删除装备方案%s吗?";
|
||||
@@ -71,9 +163,14 @@ OPTION_TOOLTIP_INTERACT_ICONS = "控制交互按键启用后NPC和物体上方
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "当你切换目标时停止自动攻击。";
|
||||
KEY_BINDING_NAME_AND_KEY = "%s(%s)";
|
||||
KEY_BINDING_TOOLTIP = "<右键解除键位>";
|
||||
RESET_POSITION = "重置位置";
|
||||
ROTATE_LEFT = "左转";
|
||||
ROTATE_RIGHT = "右转";
|
||||
ROTATE_TOOLTIP = "左键点击并拖曳以旋转角色";
|
||||
SETTINGS_BIND_KEY_TO_COMMAND_OR_CANCEL = "设置 \"%s\"的快捷键,或者按 %s 取消";
|
||||
SET_FOCUS = "设置焦点";
|
||||
FOCUSTARGET = "设定焦点目标";
|
||||
FOCUS_TOKEN_NOT_FOUND = "<无焦点>";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d件物品";
|
||||
ITEMS_EQUIPPED = "已装备%d件物品";
|
||||
ITEMS_IN_INVENTORY = "背包中有%d件物品";
|
||||
@@ -82,7 +179,6 @@ ITEM_MISSING = "%s缺失";
|
||||
ITEMS_NOT_IN_INVENTORY = "缺少%d件物品";
|
||||
STOP_AUTO_ATTACK = "停止自动攻击";
|
||||
TARGETFOCUS = "选中焦点目标";
|
||||
-- BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
COPPER_AMOUNT = "%d铜币";
|
||||
COPPER_AMOUNT_SYMBOL = "铜";
|
||||
GEARSETS_POPUP_TEXT = "输入方案名称(最多16个字符):";
|
||||
@@ -141,6 +237,8 @@ FOURTH_NUMBER = "兆";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PAPERDOLL_EQUIPMENTMANAGER = "装备管理";
|
||||
PAPERDOLL_NEWEQUIPMENTSET = "新的方案";
|
||||
PLAYER_LIST_DELIMITER = "、";
|
||||
RELOADUI = "重载界面";
|
||||
PERCENTAGE_STRING = "%d%%";
|
||||
SET_FRIENDNOTE_LABEL = "为%s设置备注:";
|
||||
SET_NOTE = "设置备注";
|
||||
@@ -220,6 +318,7 @@ SLASH_USE1 = "/use";
|
||||
SLASH_USE2 = "/use";
|
||||
SOCIAL_LABEL = "社交";
|
||||
LINK_TRADESKILL_TOOLTIP = "点击这里以创建一个链接到你的专业的快捷方式。";
|
||||
LUA_ERROR = "Lua错误";
|
||||
TEXT_TO_SPEECH = "文本转语音";
|
||||
TEXT_TO_SPEECH_ADJUST_RATE = "调节讲话速度";
|
||||
TEXT_TO_SPEECH_ADJUST_VOLUME = "音量";
|
||||
|
||||
@@ -8,6 +8,9 @@ BINDING_NAME_FOCUSTARGET = "鎖定目標";
|
||||
BINDING_NAME_INTERACTMOUSEOVER = "與滑鼠指定的目標互動";
|
||||
BINDING_NAME_INTERACTTARGET = "與目標互動";
|
||||
BINDING_NAME_TARGETFOCUS = "選定鎖定目標";
|
||||
CLASSICAPI_CURSOR = "游標";
|
||||
CLASSICAPI_EXPANDED_CURSORS = "顯示擴充游標";
|
||||
CLASSICAPI_EXPANDED_CURSORS_TOOLTIP = "指向NPC或物件時,游標會顯示你可以對其進行的操作。例如,鐵砧表示修理,! 或 ? 表示任務,魚鉤表示你的魚漂。";
|
||||
COMBAT_LABEL = "戰鬥";
|
||||
COUNTDOWN_FOR_COOLDOWNS_TEXT = "顯示冷卻時間";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "你確定要刪除裝備設定:%s嗎?";
|
||||
@@ -71,6 +74,10 @@ OPTION_TOOLTIP_INTERACT_ICONS = "在啟用互動鍵功能後,控制出現在NP
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "當你切換目標時停止自動攻擊。";
|
||||
KEY_BINDING_NAME_AND_KEY = "%s(%s)";
|
||||
KEY_BINDING_TOOLTIP = "<右鍵點擊以解除綁定>";
|
||||
RESET_POSITION = "重置位置";
|
||||
ROTATE_LEFT = "向左旋轉";
|
||||
ROTATE_RIGHT = "向右旋轉";
|
||||
ROTATE_TOOLTIP = "滑鼠左鍵點擊角色並拖曳以旋轉角度。";
|
||||
SETTINGS_BIND_KEY_TO_COMMAND_OR_CANCEL = "為\"%s\"指配綁定或是按下%s以取消";
|
||||
SET_FOCUS = "設定專注目標";
|
||||
FOCUSTARGET = "設定鎖定目標";
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Common.h"
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace Embed {
|
||||
@@ -30,4 +32,15 @@ struct Bytes {
|
||||
// resource is missing, does not inflate or split, or has no file `index`.
|
||||
Bytes File(int id, size_t index);
|
||||
|
||||
// The entry of `files`, a manifest in the generated `embedded_resources.h`,
|
||||
// whose path is the game path `path` (Common::SamePath); null when none is.
|
||||
template <class Entry, size_t N>
|
||||
const Entry *FindFile(const Entry (&files)[N], const char *path) {
|
||||
for (const Entry &file : files) {
|
||||
if (Common::SamePath(path, file.path))
|
||||
return &file;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace Embed
|
||||
|
||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user