local ADDON_NAME, RPAL = ...

-- ============================================================
-- Effect Packs — authored-particle overhaul
-- ============================================================
-- Every pack is a quiet ambience layer, not a screen filter. HOTFIX9 gives
-- rain, snow, flame, insects, petals, wisps, runes, sigils, crescents, ash,
-- blood, stars, frost, and maple leaves their own authored transparent sprite.
-- Motion stays non-interactive and sparse around the profile's readable edges.

local TEX="Interface\\AddOns\\ODYN\\Textures\\"
local CELESTIAL=TEX.."Celestial\\"
local GLOW=CELESTIAL.."starlight_glow.tga"
local COMETS={CELESTIAL.."starlight_comet_1.tga",CELESTIAL.."starlight_comet_2.tga"}
local PARTICLE=TEX.."Effects\\"
local SPRITE={
    rain=PARTICLE.."particle_rain_streak.tga",
    snow=PARTICLE.."particle_snowflake.tga",
    ember=PARTICLE.."particle_ember.tga",
    firefly=PARTICLE.."particle_firefly.tga",
    petal=PARTICLE.."particle_petal.tga",
    wisp=PARTICLE.."particle_void_wisp.tga",
    arcane=PARTICLE.."particle_arcane_rune.tga",
    gilded=PARTICLE.."particle_gilded_sigil.tga",
    moon=PARTICLE.."particle_moon_crescent.tga",
    ash=PARTICLE.."particle_ash_flake.tga",
    blood=PARTICLE.."particle_blood_drop.tga",
    star=PARTICLE.."particle_star_spark.tga",
    frost=PARTICLE.."particle_frost_shard.tga",
    maple=PARTICLE.."particle_maple_leaf.tga",
}
local TAU=math.pi*2

RPAL.EFFECT_PACKS={
    order={
        "None","Blood Drip","Rainfall","Snowfall","Emberstorm","Fireflies",
        "Falling Petals","Void Wisps","Arcane Meteors","Ashen Rain","Moon Dust",
        "Gilded Storm","Starlight Drift","Lantern Motes","Arcane Dust","Frost Sparks","Crimson Cinders","Maplefall Sparks",
    },
    costs={
        None=0,["Blood Drip"]=210,Rainfall=165,Snowfall=175,Emberstorm=220,
        Fireflies=190,["Falling Petals"]=205,["Void Wisps"]=245,
        ["Arcane Meteors"]=280,["Ashen Rain"]=185,["Moon Dust"]=260,
        ["Gilded Storm"]=340,["Starlight Drift"]=235,["Lantern Motes"]=215,["Arcane Dust"]=255,["Frost Sparks"]=240,["Crimson Cinders"]=275,["Maplefall Sparks"]=260,
    },
    descriptions={
        None="No ambient effect.",
        ["Blood Drip"]="Crimson beads gather at the binding, swell and release in restrained trails.",
        Rainfall="Authored silver-blue rain streaks lean through the outer margins in changing depths.",
        Snowfall="Six-point snow crystals drift and turn on separate, softly swaying paths.",
        Emberstorm="Hand-shaped flames corkscrew upward with a warm, irregular fading bloom.",
        Fireflies="Winged golden-green lights wander in looping paths and blink independently.",
        ["Falling Petals"]="Veined rose petals tumble, bank, and sweep through the margins in slow arcs.",
        ["Void Wisps"]="Curved violet wisps spiral through soft halos instead of generic glowing dots.",
        ["Arcane Meteors"]="Arcane comets curve in from alternating corners with true rune sparks at their wake.",
        ["Ashen Rain"]="Jagged ash flakes flutter downward at several depths, never forming a uniform curtain.",
        ["Moon Dust"]="Silver-blue crescents orbit the page in slow constellations and muted pulses.",
        ["Gilded Storm"]="Curved golden comets, compass sigils, and rising flame motes arrive in layered bursts.",
        ["Starlight Drift"]="Theme-colored stars and curved comet glints descend diagonally in a readable celestial shower.",
        ["Lantern Motes"]="Warm wandering motes and a few soft embers hover at the page edge like distant lantern light.",
        ["Arcane Dust"]="Sparse violet glyph dust and tiny stars drift in shallow arcs without crowding the profile.",
        ["Frost Sparks"]="Snow crystals and faceted ice shards move in a restrained cold shimmer.",
        ["Crimson Cinders"]="Deep red cinders rise through a handful of slow droplets for a darker ceremonial ambience.",
        ["Maplefall Sparks"]="Distinct maple leaves tumble through theme-tinted comet glints at the margins.",
    },
}

local function Clamp(value,minimum,maximum)
    if value<minimum then return minimum end
    if value>maximum then return maximum end
    return value
end

local function Smooth(value)
    value=Clamp(value,0,1)
    return value*value*(3-(2*value))
end

local function FadeWindow(age,edge)
    edge=edge or 0.18
    return math.min(Smooth(age/edge),Smooth((1-age)/edge))
end

local function SizeOf(frame)
    local width=(frame.GetWidth and frame:GetWidth()) or 900
    local height=(frame.GetHeight and frame:GetHeight()) or 600
    if not width or width<100 then width=900 end
    if not height or height<100 then height=600 end
    return width,height
end

local function Seed(index,offset)
    -- Each particle property needs a genuinely independent deterministic
    -- stream.  The old golden-ratio sequence only shifted the same stream for
    -- age and lane, so falling particles lined up as one diagonal dotted line.
    local salt=tonumber(offset) or 0
    local raw=math.sin((index+(salt*101.37))*(12.9898+(salt*7.231)))*43758.5453
    return raw-math.floor(raw)
end

local function Texture(holder,path,blend,color,width,height,layer,sublevel)
    local texture=holder:CreateTexture(nil,layer or "OVERLAY",nil,sublevel or 2)
    texture:SetTexture(path or SPRITE.star)
    if blend then texture:SetBlendMode(blend) end
    texture:SetSize(width or 8,height or width or 8)
    color=color or {1,1,1,1}
    texture:SetVertexColor(color[1] or 1,color[2] or 1,color[3] or 1,color[4] or 1)
    return texture
end

local function EdgeX(lane,width,preview)
    if preview then return (-width*0.5)+(lane*width) end
    local right=lane>=0.5
    local inward=right and ((lane-0.5)*2) or (lane*2)
    inward=inward*inward
    return right and (width*0.5-inward*width*0.34) or (-width*0.5+inward*width*0.34)
end

local function Tint(color,alpha)
    return {color[1] or 1,color[2] or 1,color[3] or 1,alpha or color[4] or 1}
end

function RPAL.RenderEffectPack(parent,name,accent,options)
    if not parent or not name or name=="None" then return nil end
    if not RPAL.EFFECT_PACKS or not RPAL.EFFECT_PACKS.costs[name] then return nil end

    options=options or {}
    accent=accent or {0.92,0.72,0.34}

    local holder=CreateFrame("Frame",nil,parent)
    holder:SetParent(parent);holder:ClearAllPoints();holder:SetAllPoints(parent)
    holder:SetClipsChildren(true);holder:EnableMouse(false);holder:EnableMouseWheel(false)
    holder._rpalEffectName=name;holder._rpalEffectVersion=6
    if parent.GetFrameStrata and holder.SetFrameStrata then holder:SetFrameStrata(parent:GetFrameStrata() or "MEDIUM") end
    if parent.GetFrameLevel and holder.SetFrameLevel then
        local offset=tonumber(options.frameLevelOffset) or (options.preview and 4 or 8)
        holder:SetFrameLevel((parent:GetFrameLevel() or 0)+offset)
    end

    local particles={}
    holder._rpalEffectParticles=particles
    -- Seed the entrance above zero and run one layout tick before returning.
    -- Newly equipped effects previously owned no anchors until their first
    -- OnUpdate, which made them depend on a later hide/show cycle in practice.
    local phase,intro,pendingUpdate=0,(options.preview and 0.55 or 0.28),0

    local function Add(kind,count,color)
        if options.preview then count=math.max(4,math.floor((count*0.42)+0.5)) end
        for index=1,count do
            local p={
                kind=kind,index=index,seed=Seed(index,0.17),lane=Seed(index,0.43),
                phase=Seed(index,0.79)*TAU,depth=0.72+(Seed(index,0.31)*0.38),
            }
            if kind=="rain" then
                local length=24+(index%5)*6
                p.core=Texture(holder,SPRITE.rain,"ADD",color,7+(index%2)*2,length,"OVERLAY",3)
                if index%3==0 then p.glow=Texture(holder,SPRITE.rain,"ADD",Tint(color,0.10),13,length+10,"ARTWORK",1) end
            elseif kind=="snow" then
                local size=6+(index%5)*1.55
                p.core=Texture(holder,SPRITE.snow,"ADD",color,size,size,"OVERLAY",3)
                if index%4==0 then p.glow=Texture(holder,GLOW,"ADD",Tint(color,0.16),size*2.3,size*2.3,"ARTWORK",1) end
            elseif kind=="ember" then
                local size=5+(index%4)*1.7
                p.core=Texture(holder,SPRITE.ember,"ADD",color,size,size*1.32,"OVERLAY",3)
                p.glow=Texture(holder,GLOW,"ADD",Tint(color,0.18),size*2.15,size*2.15,"ARTWORK",1)
            elseif kind=="firefly" then
                local size=7.5+(index%4)*0.90
                p.core=Texture(holder,SPRITE.firefly,"ADD",color,size*1.35,size,"OVERLAY",3)
                p.glow=Texture(holder,GLOW,"ADD",Tint(color,0.34),size*2.55,size*2.55,"ARTWORK",1)
                p.spark=Texture(holder,SPRITE.star,"ADD",{1.00,0.98,0.64,1.00},2.2+(index%3)*0.35,2.2+(index%3)*0.35,"OVERLAY",5)
                p.glowStrength=0.48;p.glowCap=0.42;p.sparkStrength=0.94
            elseif kind=="petal" then
                local size=9+(index%4)*2.0
                p.core=Texture(holder,SPRITE.petal,"BLEND",color,size*.76,size,"OVERLAY",3)
            elseif kind=="wisp" then
                local size=9+(index%4)*1.25
                p.core=Texture(holder,SPRITE.wisp,"ADD",color,size*.76,size*1.35,"OVERLAY",3)
                p.glow=Texture(holder,GLOW,"ADD",Tint(color,0.31),size*2.65,size*2.65,"ARTWORK",1)
                p.spark=Texture(holder,SPRITE.arcane,"ADD",{0.92,0.76,1.00,1.00},3.4+(index%2)*0.45,3.4+(index%2)*0.45,"OVERLAY",5)
                p.glowStrength=0.46;p.glowCap=0.40;p.sparkStrength=0.90
            elseif kind=="arcaneComet" or kind=="gildedComet" or kind=="themeComet" then
                local size=((kind=="gildedComet") and 58 or 48)+(index%4)*8
                p.texture=COMETS[((index-1)%#COMETS)+1]
                p.core=Texture(holder,p.texture,"ADD",color,size,size,"OVERLAY",3)
                p.glow=Texture(holder,GLOW,"ADD",Tint(color,0.10),size*0.54,size*0.54,"ARTWORK",1)
                holder._rpalUsesAuthoredComets=true
            elseif kind=="arcaneGlyph" or kind=="gildedGlyph" or kind=="moonGlyph" then
                local size=(kind=="moonGlyph" and 9 or 10)+(index%5)*(kind=="moonGlyph" and 1.0 or 1.25)
                local sprite=(kind=="moonGlyph" and SPRITE.moon) or (kind=="gildedGlyph" and SPRITE.gilded) or SPRITE.arcane
                p.core=Texture(holder,sprite,"ADD",color,size,size,"OVERLAY",3)
                if kind=="moonGlyph" or index%4==0 then
                    p.glow=Texture(holder,GLOW,"ADD",Tint(color,kind=="moonGlyph" and 0.30 or 0.10),size*(kind=="moonGlyph" and 2.45 or 2.2),size*(kind=="moonGlyph" and 2.45 or 2.2),"ARTWORK",1)
                    if kind=="moonGlyph" then
                        p.spark=Texture(holder,SPRITE.star,"ADD",{0.92,0.97,1.00,1.00},2.4+(index%3)*0.34,2.4+(index%3)*0.34,"OVERLAY",5)
                        p.glowStrength=0.44;p.glowCap=0.38;p.sparkStrength=0.90
                    end
                end
            elseif kind=="ash" then
                local size=5.5+(index%4)*1.55
                p.core=Texture(holder,SPRITE.ash,"BLEND",color,size,size*.80,"OVERLAY",3)
            elseif kind=="blood" then
                local size=6+(index%3)*1.9
                p.core=Texture(holder,SPRITE.blood,"BLEND",color,size,size*1.22,"OVERLAY",3)
                p.trail=Texture(holder,SPRITE.rain,"BLEND",Tint(color,0.68),size*.72,size*2.5,"ARTWORK",2)
                p.glow=Texture(holder,GLOW,"ADD",Tint(color,0.13),size*2.35,size*2.35,"ARTWORK",1)
                p.glowStrength=0.38;p.glowCap=0.30
            elseif kind=="star" then
                local size=5+(index%5)*1.55
                p.core=Texture(holder,SPRITE.star,"ADD",color,size,size,"OVERLAY",3)
                if index%4==0 then p.glow=Texture(holder,GLOW,"ADD",Tint(color,0.10),size*2.2,size*2.2,"ARTWORK",1) end
            elseif kind=="frostShard" then
                local size=9+(index%4)*2.1
                p.core=Texture(holder,SPRITE.frost,"ADD",color,size*.60,size*1.25,"OVERLAY",3)
                if index%3==0 then p.glow=Texture(holder,GLOW,"ADD",Tint(color,.12),size*1.65,size*1.65,"ARTWORK",1) end
            elseif kind=="maple" then
                local size=11+(index%5)*1.55
                p.core=Texture(holder,SPRITE.maple,"BLEND",color,size,size,"OVERLAY",3)
            end
            particles[#particles+1]=p
        end
    end

    if name=="Blood Drip" then
        Add("blood",30,{0.88,0.020,0.045,0.96})
    elseif name=="Rainfall" then
        Add("rain",42,{0.56,0.78,1.00,0.58})
    elseif name=="Snowfall" then
        Add("snow",58,{0.88,0.96,1.00,0.90})
    elseif name=="Emberstorm" then
        Add("ember",38,{1.00,0.39,0.055,0.88})
    elseif name=="Fireflies" then
        Add("firefly",18,{0.84,1.00,0.28,1.00})
    elseif name=="Falling Petals" then
        Add("petal",48,{1.00,0.39,0.59,0.88})
    elseif name=="Void Wisps" then
        Add("wisp",16,{0.72,0.34,1.00,1.00})
    elseif name=="Arcane Meteors" then
        Add("arcaneComet",12,{0.50,0.66,1.00,0.74})
        Add("arcaneGlyph",24,{0.72,0.42,1.00,0.72})
    elseif name=="Ashen Rain" then
        Add("ash",56,{0.76,0.72,0.69,0.76})
    elseif name=="Moon Dust" then
        Add("moonGlyph",18,{0.70,0.90,1.00,1.00})
    elseif name=="Gilded Storm" then
        Add("gildedComet",10,{1.00,0.72,0.18,0.72})
        Add("gildedGlyph",25,{1.00,0.84,0.32,0.82})
        Add("ember",17,{1.00,0.43,0.07,0.72})
    elseif name=="Starlight Drift" then
        Add("star",30,{accent[1],accent[2],accent[3],0.72})
        Add("themeComet",5,{accent[1],accent[2],accent[3],0.48})
    elseif name=="Lantern Motes" then
        Add("firefly",12,{1.00,0.78,0.28,0.92})
        Add("ember",8,{1.00,0.48,0.12,0.54})
    elseif name=="Arcane Dust" then
        Add("arcaneGlyph",20,{0.72,0.48,1.00,0.70})
        Add("star",12,{0.64,0.72,1.00,0.54})
    elseif name=="Frost Sparks" then
        Add("snow",18,{0.84,0.96,1.00,0.78})
        Add("frostShard",14,{0.58,0.86,1.00,0.82})
    elseif name=="Crimson Cinders" then
        Add("ember",18,{0.92,0.18,0.10,0.72})
        Add("blood",8,{0.72,0.015,0.035,0.62})
    elseif name=="Maplefall Sparks" then
        Add("maple",20,{accent[1],accent[2],accent[3],0.76})
        Add("themeComet",4,{accent[1],accent[2],accent[3],0.44})
        Add("gildedGlyph",5,{1.00,0.78,0.30,0.42})
    end

    local function Place(texture,x,y,alpha,rotation)
        if not texture then return end
        texture:ClearAllPoints();texture:SetPoint("CENTER",holder,"CENTER",x or 0,y or 0)
        texture:SetAlpha(Clamp(alpha or 0,0,1))
        if rotation and texture.SetRotation then texture:SetRotation(rotation) end
    end

    holder:SetAlpha(0)
    local function UpdateEffect(self,elapsed)
        if not self:IsShown() then return end
        pendingUpdate=pendingUpdate+(tonumber(elapsed) or 0)
        if pendingUpdate<(1/30) then return end
        elapsed=pendingUpdate;pendingUpdate=0
        intro=Clamp(intro+(elapsed*2.25),0,1)
        phase=phase+(elapsed*1.10)
        local width,height=SizeOf(parent)
        self:SetAlpha(Smooth(intro)*(0.94+((math.sin(phase*0.42)+1)*0.0275)))

        for _,p in ipairs(particles) do
            local age,x,y,alpha,rotation
            if p.kind=="rain" then
                age=(p.seed+phase*(0.24+(p.index%5)*0.014))%1
                x=EdgeX(p.lane,width,options.preview)-(age*height*0.10)+math.sin(p.phase)*6
                y=(height*0.59)-(age*height*1.18)
                alpha=FadeWindow(age,0.12)*(0.28+(p.depth-0.72)*0.28);rotation=-0.12
            elseif p.kind=="snow" then
                age=(p.seed+phase*(0.060+(p.index%5)*0.0050))%1
                x=(-width*0.47)+(p.lane*width*0.94)+math.sin(phase*(0.58+p.index%3*0.05)+p.phase)*(17+p.index%4*4)
                y=(height*0.58)-(age*height*1.16)
                alpha=FadeWindow(age,0.17)*(0.61+(p.depth-0.72)*0.43)
                rotation=(phase*(0.13+(p.index%4)*0.03)+p.phase)%TAU
            elseif p.kind=="ember" then
                age=(p.seed+phase*(0.070+(p.index%4)*0.008))%1
                x=EdgeX(p.lane,width,options.preview)+math.sin(phase*1.22+p.phase)*(10+p.index%4*3)+math.sin(age*math.pi)*18*((p.index%2==0) and 1 or -1)
                y=(-height*0.58)+(age*height*1.08)
                alpha=FadeWindow(age,0.17)*(1-age*0.42)*0.70;rotation=(phase*0.42+p.phase)%TAU
            elseif p.kind=="firefly" then
                x=(-width*0.44)+(p.lane*width*0.88)+math.sin(phase*(0.27+(p.index%3)*0.022)+p.phase)*(width*0.095)+math.sin(phase*0.63+p.phase*1.9)*18
                y=(-height*0.43)+(p.seed*height*0.86)+math.sin(phase*(0.34+(p.index%4)*0.020)+p.phase*1.7)*(height*0.18)
                local blink=(math.sin(phase*(1.55+(p.index%4)*0.14)+p.phase)+1)*0.5
                alpha=0.58+(blink*0.42)
            elseif p.kind=="petal" then
                age=(p.seed+phase*(0.052+(p.index%4)*0.0055))%1
                x=(-width*0.48)+(p.lane*width*0.96)+math.sin(phase*0.64+p.phase)*(29+p.index%4*5)+math.sin(age*math.pi)*24
                y=(height*0.58)-(age*height*1.16)+math.sin(age*TAU+p.phase)*8
                alpha=FadeWindow(age,0.16)*0.76;rotation=(phase*(0.62+(p.index%3)*0.12)+p.phase)%TAU
            elseif p.kind=="wisp" then
                local orbit=phase*(0.31+(p.index%4)*0.022)+p.phase
                x=(-width*0.42)+(p.lane*width*0.84)+math.cos(orbit)*(width*(0.082+p.seed*0.060))+math.sin(orbit*2.1)*26
                y=(-height*0.41)+(p.seed*height*0.82)+math.sin(orbit)*(height*(0.15+p.lane*0.065))
                local pulse=(math.sin(phase*1.15+p.phase)+1)*0.5
                alpha=0.48+(pulse*0.46);rotation=(orbit*0.65)%TAU
            elseif p.kind=="arcaneComet" or p.kind=="gildedComet" then
                age=(p.seed+phase*(0.052+(p.index%4)*0.006))%1
                local side=(p.index%2==0) and 1 or -1
                local arc=math.sin(age*math.pi)
                x=side*((width*0.58)-(age*width*0.76))+(side*arc*width*0.13)+(p.lane-0.5)*width*0.10
                y=(height*0.60)-(age*height*0.86)+(arc*height*0.20)+((p.lane-0.5)*height*0.24)
                alpha=FadeWindow(age,0.13)*((p.kind=="gildedComet") and 0.48 or 0.54)
                rotation=(side>0 and 0.62 or -0.92)+(arc*side*0.24)
            elseif p.kind=="arcaneGlyph" then
                age=(p.seed+phase*(0.044+(p.index%5)*0.004))%1
                local side=(p.index%2==0) and 1 or -1
                x=side*(width*0.46-age*width*0.44)+math.sin(age*math.pi+p.phase)*width*0.08
                y=(height*0.46-age*height*0.62)+((p.lane-0.5)*height*0.45)+math.sin(age*TAU+p.phase)*18
                alpha=FadeWindow(age,0.16)*(0.23+((math.sin(phase*1.35+p.phase)+1)*0.10))
                rotation=(phase*0.26+p.phase)%TAU
            elseif p.kind=="gildedGlyph" then
                local orbit=phase*(0.18+(p.index%4)*0.014)+p.phase
                local side=(p.index%2==0) and 1 or -1
                local radiusX=width*(0.32+(p.seed*0.16));local radiusY=height*(0.20+(p.lane*0.16))
                x=side*math.cos(orbit)*radiusX
                y=math.sin(orbit)*radiusY+math.sin(orbit*2.0)*height*0.06
                alpha=0.11+((math.sin(phase*1.46+p.phase)+1)*0.21);rotation=(orbit*1.4)%TAU
            elseif p.kind=="moonGlyph" then
                local orbit=phase*(0.17+(p.index%4)*0.014)+p.phase
                x=(-width*0.42)+(p.lane*width*0.84)+math.cos(orbit)*(width*(0.082+p.seed*0.060))
                y=math.sin(orbit)*(height*(0.23+p.lane*0.12))+((p.seed-0.5)*height*0.40)
                local pulse=(math.sin(phase*1.28+p.phase)+1)*0.5
                alpha=0.50+(pulse*0.44);rotation=(orbit*0.78)%TAU
            elseif p.kind=="ash" then
                age=(p.seed+phase*(0.050+(p.index%5)*0.0045))%1
                x=(-width*0.47)+(p.lane*width*0.94)+math.sin(phase*0.46+p.phase)*(22+p.index%5*4)+math.sin(age*math.pi)*14
                y=(height*0.57)-(age*height*1.14)
                alpha=FadeWindow(age,0.18)*(0.39+(p.depth-0.72)*0.48);rotation=(phase*(0.28+(p.index%4)*0.055)+p.phase)%TAU
            elseif p.kind=="blood" then
                age=(p.seed+phase*(0.062+(p.index%4)*0.0060))%1
                local side=(p.index%2==0) and 1 or -1
                if p.index%3==0 then
                    x=(-width*0.36)+(p.lane*width*0.72)+math.sin(p.phase+phase*0.42)*5
                    y=(height*0.47)-(age*(height*(0.44+(p.index%4)*0.055)))
                else
                    x=side*(width*0.47-((p.index%5)*8))+math.sin(p.phase+phase*0.42)*4
                    y=(height*0.43)-(age*(height*(0.58+(p.index%4)*0.045)))
                end
                alpha=FadeWindow(age,0.12)*(0.72+(age*0.20))
                if p.core then local size=6+(age*5)+(p.index%3);p.core:SetSize(size,size) end
                if p.trail then p.trail:SetSize(4.5+(p.index%3),14+(age*22)+(p.index%4)*3) end
            elseif p.kind=="star" then
                age=(p.seed+phase*(0.044+(p.index%5)*0.0048))%1
                x=(-width*0.47)+(p.lane*width*0.94)+((age-0.5)*width*0.08)+math.sin(age*math.pi+p.phase)*24
                y=(height*0.61)-(age*height*1.22)+((p.lane-0.5)*height*0.20)
                alpha=FadeWindow(age,0.14)*(0.16+((math.sin(phase*1.10+p.phase)+1)*0.09))
                rotation=(phase*(0.12+(p.index%4)*0.018)+p.phase)%TAU
            elseif p.kind=="frostShard" then
                age=(p.seed+phase*(0.048+(p.index%5)*0.0042))%1
                local side=(p.index%2==0) and 1 or -1
                x=side*(width*.46-(p.lane*width*.14))+math.sin(phase*.44+p.phase)*(18+p.index%4*4)
                y=(height*.56)-(age*height*1.12)+math.sin(age*TAU+p.phase)*10
                local shimmer=(math.sin(phase*1.8+p.phase)+1)*.5
                alpha=FadeWindow(age,.15)*(.42+(shimmer*.40))
                rotation=(p.phase*.18)+(math.sin(phase*.52+p.phase)*.34)
            elseif p.kind=="maple" then
                age=(p.seed+phase*(.046+(p.index%4)*.0048))%1
                local wind=math.sin(age*math.pi)
                x=(-width*.50)+(p.lane*width)+math.sin(phase*.55+p.phase)*(28+p.index%5*5)+(wind*width*.10)
                y=(height*.59)-(age*height*1.18)+math.sin(age*TAU+p.phase)*13
                alpha=FadeWindow(age,.16)*(.54+(p.depth-.72)*.35)
                rotation=(phase*(.42+(p.index%3)*.09)+p.phase+(wind*.55))%TAU
            elseif p.kind=="themeComet" then
                age=(p.seed+phase*(0.058+(p.index%3)*0.006))%1
                local arc=math.sin(age*math.pi)
                x=(-width*0.62)+(age*width*1.24)+((p.lane-0.5)*width*0.20)+(arc*width*0.07)
                y=(height*0.62)-(age*height*1.24)+((p.lane-0.5)*height*0.24)+(arc*height*0.10)
                alpha=FadeWindow(age,0.11)*(0.22+((p.index%2)*0.05));rotation=-0.72+(arc*0.20)
            end

            Place(p.core,x,y,alpha,rotation)
            if p.glow then Place(p.glow,x,y,Clamp((alpha or 0)*(p.glowStrength or 0.30),0,p.glowCap or 0.26),rotation) end
            if p.spark then Place(p.spark,x,y,Clamp((alpha or 0)*(p.sparkStrength or 1),0,1),rotation) end
            if p.trail then Place(p.trail,x,(y or 0)+8,Clamp((alpha or 0)*0.62,0,0.58),rotation) end
        end
    end
    holder:SetScript("OnUpdate",UpdateEffect)
    -- Place every particle immediately.  This removes the old dependency on
    -- the frame receiving a later OnUpdate after the ledger/inspect reopened.
    UpdateEffect(holder,1/30)

    return holder
end
