Add trainer planning and level-up tracking

This commit is contained in:
DuvelCorp
2026-09-11 13:45:58 +02:00
parent aeaddd9558
commit 0fb52f67ff
15 changed files with 3416 additions and 70 deletions
+328 -15
View File
@@ -33,6 +33,7 @@ MTH_FX_DBG = {
-- Sections shown, in order, in the options panel. `titleKey` localizes `title`.
MTH_FX_ANIM_SECTIONS = {
{ id = "pet", title = "Pet", titleKey = "ANIM_SECTION_PET" },
{ id = "level", title = "Leveling", titleKey = "ANIM_SECTION_LEVEL" },
{ id = "combat", title = "Combat", titleKey = "ANIM_SECTION_COMBAT" },
{ id = "book", title = "Book", titleKey = "ANIM_SECTION_BOOK" },
{ id = "ui", title = "Interface", titleKey = "ANIM_SECTION_UI" },
@@ -43,6 +44,18 @@ MTH_FX_ANIM_SECTIONS = {
-- labelKey/tooltipKey localize label/tooltip (resolved at render time).
-- Adding a new animation = one entry here + one MTH_FX_IsEnabled("key") guard.
MTH_FX_ANIM_DEFS = {
{ key = "level.hunter", section = "level", default = true,
label = "Celebrate Hunter training levels",
tooltip = "Show a banner and burst when a level unlocks Hunter trainer spells." },
{ key = "level.huntermilestone", section = "level", default = true,
label = "Celebrate Hunter milestones",
tooltip = "Use a large celebration at levels 10, 20, 30, 40, 50, and 60." },
{ key = "level.pet", section = "level", default = true,
label = "Celebrate pet training levels",
tooltip = "Show a banner when your current pet gains a level with Pet Trainer spells." },
{ key = "level.loyalty", section = "level", default = true,
label = "Celebrate pet loyalty gains",
tooltip = "Show a golden banner when your current pet gains a loyalty level." },
{ key = "pet.celebrate", section = "pet", default = true,
label = "Celebrate new pet abilities", labelKey = "ANIM_PET_CELEBRATE",
tooltip = "Play a firework and a banner in the middle of the screen when your pet learns a new ability or a new rank.",
@@ -244,6 +257,27 @@ if Sauce and type(Sauce.RegisterType) == "function" and not Sauce.MTH_HasShake t
MTH_FX_DBG.shakeErr = err
end
if Sauce and type(Sauce.RegisterType) == "function" and not Sauce.MTH_HasSpiral then
local ok, err = pcall(function()
Sauce.MTH_HasSpiral = true
Sauce:RegisterType("mth_spiral", {
init = function(target, from)
return from or 0
end,
apply = function(target, from, to, t, tween)
local radius = (tween.radius or 0) * (1 - t)
local angle = (tween.startAngle or 0) + (tween.rotations or 1) * 2 * math.pi * t
target:ClearAllPoints()
target:SetPoint("CENTER", tween.anchor or UIParent, "CENTER",
(tween.cx or 0) + math.cos(angle) * radius,
(tween.cy or 0) + math.sin(angle) * radius)
end,
})
end)
MTH_FX_DBG.spiralOk = ok
MTH_FX_DBG.spiralErr = err
end
-- Reusable flash: overlays a short additive colour glow on any frame/button and
-- fades it out. Safe on any frame that supports CreateTexture; silent no-op
-- otherwise. The overlay texture is cached on the frame for reuse.
@@ -277,11 +311,15 @@ end
local MTH_FX_NUM_SPARKS = 22
local MTH_FX_NUM_CONFETTI = 48
local MTH_FX_NUM_BLACKHOLE = 40
local MTH_FX_Container
local MTH_FX_MsgFrame
local MTH_FX_TitleFS
local MTH_FX_SubFS
local MTH_FX_Sparks = {}
local MTH_FX_Confetti = {}
local MTH_FX_BlackHole = {}
-- Palette for the firework sparks (r, g, b), warm celebratory tones.
local MTH_FX_Colors = {
@@ -337,33 +375,75 @@ local function MTH_FX_EnsureFrames()
i = i + 1
end
i = 1
while i <= MTH_FX_NUM_CONFETTI do
local bit = MTH_FX_Container:CreateTexture(nil, "OVERLAY")
bit:SetTexture("Interface\\Buttons\\WHITE8X8")
bit:SetWidth(5)
bit:SetHeight(8)
bit:SetPoint("CENTER", MTH_FX_Container, "CENTER", 0, 0)
bit:Hide()
MTH_FX_Confetti[i] = bit
i = i + 1
end
i = 1
while i <= MTH_FX_NUM_BLACKHOLE do
local particle = MTH_FX_Container:CreateTexture(nil, "OVERLAY")
particle:SetTexture("Interface\\Cooldown\\star4")
particle:SetWidth(10)
particle:SetHeight(10)
particle:SetBlendMode("ADD")
particle:SetPoint("CENTER", MTH_FX_Container, "CENTER", 0, 0)
particle:Hide()
MTH_FX_BlackHole[i] = particle
i = i + 1
end
return true
end
-- Public entry point: play a celebratory message + firework burst in the middle
-- of the screen. `title` and `subtitle` are optional strings.
function MTH_Celebrate(title, subtitle)
-- Public entry point: play a full-screen celebration. `style` defaults to the
-- original firework burst and may be firework, victory, starfall, fanfare, or comet.
function MTH_Celebrate(title, subtitle, style, displaySeconds)
if not Sauce then
return
end
if not MTH_FX_EnsureFrames() then
return
end
style = string.lower(tostring(style or "firework"))
local fadeDelay = tonumber(displaySeconds) or 2.1
if fadeDelay < 0.5 then fadeDelay = 0.5 end
MTH_FX_TitleFS:SetTextColor(1, 0.82, 0)
if style == "starfall" then
MTH_FX_TitleFS:SetTextColor(0.45, 0.80, 1.00)
else
MTH_FX_TitleFS:SetTextColor(1, 0.82, 0)
end
MTH_FX_TitleFS:SetText(title or "")
MTH_FX_SubFS:SetText(subtitle or "")
Sauce:Stop(MTH_FX_MsgFrame)
MTH_FX_MsgFrame:SetAlpha(0)
MTH_FX_MsgFrame:Show()
Sauce:Sequence({
Sauce:Group({
Sauce:FadeTo(MTH_FX_MsgFrame, 1, 0.25),
Sauce:Pulse(MTH_FX_MsgFrame, 1.22, 0.5),
}),
Sauce:FadeOut(MTH_FX_MsgFrame, 0.6, "inQuad", { delay = 2.2 }),
})
if style == "victory" or style == "fanfare" then
Sauce:Sequence({
Sauce:Group({
Sauce:SlideIn(MTH_FX_MsgFrame, "UP", 80, 0.36, "outBack", { defer = true }),
Sauce:Pulse(MTH_FX_MsgFrame, style == "victory" and 1.32 or 1.18, 0.52, { defer = true }),
}),
Sauce:FadeOut(MTH_FX_MsgFrame, 0.6, "inQuad", { delay = fadeDelay }),
})
else
Sauce:Sequence({
Sauce:Group({
Sauce:FadeTo(MTH_FX_MsgFrame, 1, 0.25),
Sauce:Pulse(MTH_FX_MsgFrame, 1.22, 0.5),
}),
Sauce:FadeOut(MTH_FX_MsgFrame, 0.6, "inQuad", { delay = fadeDelay }),
})
end
MTH_FX_Container:Show()
local colorCount = table.getn(MTH_FX_Colors)
@@ -371,34 +451,70 @@ function MTH_Celebrate(title, subtitle)
local i = 1
while i <= MTH_FX_NUM_SPARKS do
local spark = MTH_FX_Sparks[i]
spark:SetTexture("Interface\\Cooldown\\star4")
spark:SetTexCoord(0, 1, 0, 1)
local startX, startY = 0, 0
local delay, gravity = 0, 300
local angle = (i / MTH_FX_NUM_SPARKS) * 2 * math.pi + (math.random() - 0.5) * 0.4
local speed = 150 + math.random() * 130
local vx = math.cos(angle) * speed
local vy = math.sin(angle) * speed + 70
local dur = 0.9 + math.random() * 0.4
if style == "victory" then
startX = ((i - math.floor(i / 2) * 2) == 0) and -130 or 130
speed = 100 + math.random() * 100
vx = math.cos(angle) * speed
vy = math.sin(angle) * speed + 105
elseif style == "starfall" then
startX = -230 + math.random() * 460
startY = 200 + math.random() * 100
vx = -35 + math.random() * 70
vy = -10 + math.random() * 25
gravity = 235
dur = 1.1 + math.random() * 0.5
elseif style == "fanfare" then
delay = ((i - 1) - math.floor((i - 1) / 3) * 3) * 0.18
speed = 110 + math.random() * 90
vx = math.cos(angle) * speed
vy = math.sin(angle) * speed + 90
elseif style == "comet" then
if i == 1 then
startY = -180
vx, vy, gravity, dur = 0, 430, 0, 0.52
else
delay = 0.5
startY = 45
speed = 130 + math.random() * 120
vx = math.cos(angle) * speed
vy = math.sin(angle) * speed + 70
end
end
colorIdx = colorIdx + 1
if colorIdx > colorCount then colorIdx = 1 end
local color = MTH_FX_Colors[colorIdx]
spark:SetVertexColor(color[1], color[2], color[3])
spark:SetAlpha(1)
spark:SetAlpha(delay > 0 and 0 or 1)
spark:Show()
Sauce:Stop(spark)
Sauce:Group({
Sauce:Tween(spark, {
type = "mth_ballistic",
from = { 0, 0 },
from = { startX, startY },
duration = dur,
easing = "linear",
delay = delay,
anchor = MTH_FX_Container,
dur = dur,
vx = vx,
vy = vy,
gravity = 300,
gravity = gravity,
}),
Sauce:FadeTo(spark, 1, 0.05, "outQuad", { delay = delay }),
Sauce:FadeTo(spark, 0, 0.45, "inQuad", {
delay = dur - 0.45,
delay = delay + dur - 0.45,
onFinish = function()
spark:Hide()
end,
@@ -408,6 +524,203 @@ function MTH_Celebrate(title, subtitle)
end
end
local function MTH_FX_ShowPreviewBanner(title, subtitle, r, g, b)
MTH_FX_TitleFS:SetTextColor(r, g, b)
MTH_FX_TitleFS:SetText(title)
MTH_FX_SubFS:SetText(subtitle)
Sauce:Stop(MTH_FX_MsgFrame)
MTH_FX_MsgFrame:SetAlpha(0)
MTH_FX_MsgFrame:Show()
Sauce:Sequence({
Sauce:Group({
Sauce:FadeTo(MTH_FX_MsgFrame, 1, 0.18),
Sauce:Pulse(MTH_FX_MsgFrame, 1.18, 0.45),
}),
Sauce:FadeOut(MTH_FX_MsgFrame, 0.55, "inQuad", { delay = 2.1 }),
})
end
function MTH_CelebrateConfetti(title, subtitle)
if not Sauce or not MTH_FX_EnsureFrames() then return end
MTH_FX_ShowPreviewBanner(title or "Celebration!", subtitle or "Confetti", 1, 0.82, 0.15)
MTH_FX_Container:Show()
local count = table.getn(MTH_FX_Colors)
local i = 1
while i <= MTH_FX_NUM_CONFETTI do
local bit = MTH_FX_Confetti[i]
local color = MTH_FX_Colors[((i - 1) - math.floor((i - 1) / count) * count) + 1]
local size = 3 + math.random() * 5
local angle = math.random() * 2 * math.pi
local speed = 90 + math.random() * 210
local dur = 1.25 + math.random() * 0.7
bit:SetWidth(size)
bit:SetHeight(size * 1.6)
bit:SetVertexColor(color[1], color[2], color[3])
bit:SetAlpha(1)
bit:Show()
Sauce:Stop(bit)
Sauce:Group({
Sauce:Tween(bit, {
type = "mth_ballistic", from = { 0, 0 }, duration = dur,
easing = "linear", anchor = MTH_FX_Container, dur = dur,
vx = math.cos(angle) * speed, vy = math.sin(angle) * speed + 100, gravity = 255,
}),
Sauce:FadeTo(bit, 0, 0.35, "inQuad", {
delay = dur - 0.35, onFinish = function() bit:Hide() end,
}),
})
i = i + 1
end
end
function MTH_CelebrateBlackHole(title, subtitle)
if not Sauce or not MTH_FX_EnsureFrames() then return end
MTH_FX_ShowPreviewBanner(title or "Gravity Well", subtitle or "Black Hole", 0.68, 0.45, 1.00)
MTH_FX_Container:Show()
local colors = { { 0.45, 0.25, 1 }, { 0.30, 0.50, 1 }, { 0.80, 0.55, 1 } }
local i = 1
while i <= MTH_FX_NUM_BLACKHOLE do
local particle = MTH_FX_BlackHole[i]
local color = colors[((i - 1) - math.floor((i - 1) / 3) * 3) + 1]
local startAngle = math.random() * 2 * math.pi
local radius = 95 + math.random() * 145
local duration = 1.5 + math.random() * 1.0
local size = 7 + math.random() * 8
particle:SetWidth(size)
particle:SetHeight(size)
particle:SetVertexColor(color[1], color[2], color[3])
particle:SetAlpha(1)
particle:Show()
Sauce:Stop(particle)
Sauce:Group({
Sauce:Tween(particle, {
type = "mth_spiral", from = 0, to = 1, duration = duration,
easing = "inQuad", anchor = MTH_FX_Container, cx = 0, cy = 0,
radius = radius, startAngle = startAngle, rotations = 1.5 + math.random() * 2.0,
}),
Sauce:Size(particle, { size, size }, { 2, 2 }, duration, "inQuad"),
Sauce:FadeTo(particle, 0, duration * 0.3, "inQuad", {
delay = duration * 0.7, onFinish = function() particle:Hide() end,
}),
})
i = i + 1
end
end
function MTH_CelebrateShockwave(title, subtitle)
if not Sauce or not MTH_FX_EnsureFrames() then return end
MTH_FX_ShowPreviewBanner(title or "Impact!", subtitle or "Shockwave", 1, 0.62, 0.15)
MTH_FX_Container:Show()
local dotsPerWave = 16
local i = 1
while i <= MTH_FX_NUM_CONFETTI do
local dot = MTH_FX_Confetti[i]
local wave = math.floor((i - 1) / dotsPerWave) + 1
local pos = (i - 1) - math.floor((i - 1) / dotsPerWave) * dotsPerWave
local angle = pos / dotsPerWave * 2 * math.pi
local duration = 1.1 + wave * 0.18
local radius = 115 + wave * 52
local delay = (wave - 1) * 0.18
local intensity = 1 - (wave - 1) * 0.20
dot:SetWidth(5)
dot:SetHeight(5)
dot:SetVertexColor(1 * intensity, 0.65 * intensity, 0.18 * intensity)
dot:SetAlpha(0)
dot:Show()
Sauce:Stop(dot)
Sauce:Group({
Sauce:Tween(dot, {
type = "mth_ballistic", from = { 0, 0 }, duration = duration,
easing = "outQuad", delay = delay, anchor = MTH_FX_Container, dur = duration,
vx = math.cos(angle) * radius / duration, vy = math.sin(angle) * radius / duration, gravity = 0,
}),
Sauce:FadeTo(dot, 1, 0.04, "outQuad", { delay = delay }),
Sauce:FadeTo(dot, 0, 0.4, "inQuad", {
delay = delay + duration - 0.4, onFinish = function() dot:Hide() end,
}),
})
i = i + 1
end
end
function MTH_CelebrateShatter(title, subtitle)
if not Sauce or not MTH_FX_EnsureFrames() then return end
MTH_FX_ShowPreviewBanner(title or "Shattered!", subtitle or "Shatter", 0.70, 0.85, 1.00)
MTH_FX_Container:Show()
local grid = 4
local iconSize = 80
local fragmentSize = iconSize / grid
local i = 1
while i <= MTH_FX_NUM_SPARKS do
local spark = MTH_FX_Sparks[i]
if i <= grid * grid then
local row = math.floor((i - 1) / grid)
local col = (i - 1) - row * grid
local x = (col - grid / 2 + 0.5) * fragmentSize
local y = ((grid - 1 - row) - grid / 2 + 0.5) * fragmentSize
local duration = 1.1 + math.random() * 0.45
spark:SetTexture("Interface\\Icons\\Ability_Hunter_SniperShot")
spark:SetTexCoord(col / grid, (col + 1) / grid, row / grid, (row + 1) / grid)
spark:SetWidth(fragmentSize)
spark:SetHeight(fragmentSize)
spark:SetVertexColor(1, 1, 1)
spark:SetAlpha(1)
spark:Show()
Sauce:Stop(spark)
Sauce:Group({
Sauce:Tween(spark, {
type = "mth_ballistic", from = { x, y }, duration = duration,
easing = "linear", anchor = MTH_FX_Container, dur = duration,
vx = x * 4 + (math.random() - 0.5) * 90,
vy = y * 4 + 95 + math.random() * 80, gravity = 265,
}),
Sauce:FadeTo(spark, 0, 0.35, "inQuad", {
delay = duration - 0.35, onFinish = function() spark:Hide() end,
}),
})
end
i = i + 1
end
end
function MTH_CelebratePreview(style)
style = string.lower(tostring(style or "firework"))
if style == "all" then
if not Sauce then return end
local styles = { "firework", "blackhole", "shockwave", "shatter" }
local steps = {}
local i = 1
while i <= table.getn(styles) do
local previewStyle = styles[i]
table.insert(steps, Sauce:Run(function()
MTH_CelebratePreview(previewStyle)
end, { defer = true }))
table.insert(steps, Sauce:Delay(3.7))
i = i + 1
end
Sauce:Sequence(steps)
return true
end
if style == "blackhole" then
MTH_CelebrateBlackHole("Celebration Preview", "Black Hole")
return true
elseif style == "shockwave" then
MTH_CelebrateShockwave("Celebration Preview", "Shockwave")
return true
elseif style == "shatter" then
MTH_CelebrateShatter("Celebration Preview", "Shatter")
return true
end
local labels = {
firework = "Firework Burst",
}
if not labels[style] then return false end
MTH_Celebrate("Celebration Preview", labels[style], style)
return true
end
-- Convenience wrapper used when the hunter's pet learns a new ability/rank.
function MTH_CelebratePetAbility(label)
if not MTH_FX_IsEnabled("pet.celebrate") then