mirror of
https://github.com/brues-code/pfUI.git
synced 2026-09-24 16:46:04 +00:00
5bede33f00
The mana spark modelled a tick clock that does not exist. Spending mana restarted the sweep with a five-second span, then it cycled at two seconds until the next spend -- so every cast reset the phase, and the spark drew a period the server never runs. There is one regen timer for every power, REGEN_TIME_FULL (2s), re-armed with `+=` in Player::RegenerateAll and never touched by casting. The five-second rule is a separate countdown that SetLastManaUse arms on any mana-costing cast; it changes what a tick pays, never when ticks land. Inside the window the tick still fires and still pays mp5 plus whatever share of spirit the player keeps. That share is common and not computable here. The amounts are readable now (C_Spell.GetSpellEffectInfo), and the talent, racial and buff sources could be enumerated -- but the Casting Regen item ladder is equip auras that never appear in the buff list, and m_modManaRegenInterrupt is never sent. A floor that is wrong whenever the player wears such an item is worse than showing what happens: the spark starts an FSR window dim and goes solid on the first tick that lands inside it. The sweep is a free-running phase lock on that one clock. Every positive mana delta used to re-anchor it, and most are not the tick -- Illumination refunds, Judgement of Wisdom, potions, a Mana Spring totem on its own 2s clock at another phase -- so the spark snapped back to the edge mid-sweep several times a cycle. A gain re-anchors only if it lands within 250ms of the predicted boundary. Two rejected gains exactly one period apart are the real clock and move the lock, which is how a wrong first lock heals. Rollover advances by whole periods from the anchor instead of restarting from now, so frame timing no longer accumulates into drift the lock then has to chase. Even a correct tick used to hitch the spark: re-anchoring to arrival time moved it by the jitter, 20-80ms, right at the wrap. Inside an 80ms band the tick now confirms the sweep and leaves the anchor alone; only the excess is pulled in. With no self-drift left there is nothing systematic in that band. The rule is armed from UNIT_SPELLCAST_SUCCEEDED filtered through C_Spell.GetSpellPowerCost, mirroring Spell::TakePower's condition, rather than from a mana decrease -- Mana Burn lowers mana without arming the rule. UNIT_SPELLCAST_CHANNEL_STOP re-arms it, since Unit::Update will not expire it while the spending spell is still channeling. The energy period no longer names a spell. Player::RegenerateAll sums SPELL_AURA_MOD_ENERGY_REGEN_TIME and takes amount * agility / 10 off the timer in milliseconds; everything in that sum is readable here. The player's own spells carrying aura 217 come from C_SpellBook.GetPlayerSpellsByAura, passives count while known (current rank only), anything else while it is up in the buff list, and each amount is basePoints + baseDice from the effect data. So Blade Rush is found without GetTalentInfo(2, 16) -- an ordinal into the talent grid that does not fail when the tree changes but reads another talent's rank (index 16 is Vigor one tab over) -- and a retune or a second source needs no edit. The sum is cached and dropped on SPELLS_CHANGED and PLAYER_AURAS_CHANGED, the two events its two halves move on, so a tick costs no spellbook walk; agility stays live, it is one call. The CHAT_MSG_SPELL_SELF_BUFF "You gain ... Energy from" filter is gone. The phase lock rejects Relentless Strikes and Thistle Tea the same way it rejects a paladin's refunds, it was English-only, and its ignore-the-next-gain logic could eat a real tick if events reordered.
262 lines
9.4 KiB
Lua
262 lines
9.4 KiB
Lua
-- One server clock drives every power: Player::RegenerateAll fires every
|
|
-- REGEN_TIME_FULL (2s), re-arms with `+=`, and is never reset by casting. The
|
|
-- five-second rule (SetLastManaUse on any mana-costing cast) changes what a tick
|
|
-- pays, never when it lands; mp5 and the player's MOD_MANA_REGEN_INTERRUPT share
|
|
-- still come in. That share can't be computed here -- item sources are equip
|
|
-- auras absent from the buff list and m_modManaRegenInterrupt is never sent --
|
|
-- so the spark shows it instead: dim through the window until a tick lands.
|
|
--
|
|
-- The sweep free-runs on that clock and phase-locks to observed gains. A gain
|
|
-- mid-sweep (Illumination, Judgement of Wisdom, potions, a Mana Spring totem on
|
|
-- its own phase) is not the tick and never moves it.
|
|
|
|
local FIVE_SECOND_RULE = 5
|
|
|
|
-- gains farther than this from the predicted boundary are not the tick
|
|
local TICK_TOLERANCE = .25
|
|
|
|
-- arrival jitter; a tick inside this band confirms the sweep rather than
|
|
-- re-anchoring it, or the spark hitches at every wrap
|
|
local TICK_JITTER = .08
|
|
|
|
-- Player::RegenerateAll:
|
|
-- mod = GetTotalAuraModifier(SPELL_AURA_MOD_ENERGY_REGEN_TIME)
|
|
-- if mod > 0 then mod = mod * agility / 10 end
|
|
-- m_regenTimer += max(1, REGEN_TIME_FULL - mod) -- milliseconds
|
|
local REGEN_TIME_FULL = 2
|
|
local ENERGY_REGEN_TIME_AURA = 217 -- SPELL_AURA_MOD_ENERGY_REGEN_TIME
|
|
|
|
-- fixed magnitude is basePoints + baseDice (stored 11 -> 12); a die above 1 is
|
|
-- a roll the client can't know, so it counts as nothing rather than a guess
|
|
local amountCache = {}
|
|
local function auraAmount(spellID)
|
|
local amount = amountCache[spellID]
|
|
if amount then return amount end
|
|
amount = 0
|
|
local effects = C_Spell.GetSpellEffectInfo(spellID) -- nil for an id with no record
|
|
if effects then
|
|
for i = 1, 3 do
|
|
local fx = effects[i]
|
|
if fx.auraName == ENERGY_REGEN_TIME_AURA and fx.dieSides <= 1 then
|
|
amount = fx.basePoints + fx.baseDice
|
|
break
|
|
end
|
|
end
|
|
end
|
|
amountCache[spellID] = amount
|
|
return amount
|
|
end
|
|
|
|
-- A passive is in effect exactly while known (current rank only, never in the
|
|
-- buff list); anything castable or cast on us counts only while it is up.
|
|
local function getEnergyRegenTimeMod()
|
|
local sum = 0
|
|
for _, spellID in ipairs(C_SpellBook.GetPlayerSpellsByAura(ENERGY_REGEN_TIME_AURA)) do
|
|
if C_Spell.IsSpellPassive(spellID) then
|
|
sum = sum + auraAmount(spellID)
|
|
end
|
|
end
|
|
for i = 1, 32 do
|
|
local spellID = select(10, C_UnitAuras.UnitAura("player", i, "HELPFUL"))
|
|
if not spellID then break end
|
|
sum = sum + auraAmount(spellID)
|
|
end
|
|
return sum
|
|
end
|
|
|
|
-- cleared on SPELLS_CHANGED (passives) and PLAYER_AURAS_CHANGED (buffs), and
|
|
-- recomputed by the next tick that asks. Agility stays live: it's one call.
|
|
local energyRegenTimeMod
|
|
|
|
local function getAdjustedTickTimer()
|
|
if not energyRegenTimeMod then
|
|
energyRegenTimeMod = getEnergyRegenTimeMod()
|
|
end
|
|
if energyRegenTimeMod == 0 then return REGEN_TIME_FULL end
|
|
|
|
-- ms on the server, seconds here; the 1ms floor is the server's and this is a divisor
|
|
local reduction = energyRegenTimeMod * UnitStat("player", 2) / 10000
|
|
return math.max(0.001, REGEN_TIME_FULL - reduction)
|
|
end
|
|
|
|
pfUI:RegisterModule("energytick", function()
|
|
if not pfUI.uf or not pfUI.uf.player then
|
|
return
|
|
end
|
|
|
|
-- inside the module body on purpose: C is on pfUI.env, not _G
|
|
local function getBarWidth()
|
|
return C.unitframes.player.pwidth ~= "-1" and C.unitframes.player.pwidth or C.unitframes.player.width
|
|
end
|
|
|
|
-- was this gain the regen tick? if so, re-anchor the sweep on it
|
|
local function lockTick(frame)
|
|
local now, period = GetTime(), getAdjustedTickTimer()
|
|
|
|
if frame.start then
|
|
-- signed distance to the nearest predicted boundary
|
|
local err = mod(now - frame.start, period)
|
|
if err > period / 2 then err = err - period end
|
|
|
|
if math.abs(err) <= TICK_TOLERANCE then
|
|
-- correct only what lies beyond normal jitter
|
|
if err > TICK_JITTER then
|
|
frame.start = frame.start + (err - TICK_JITTER)
|
|
elseif err < -TICK_JITTER then
|
|
frame.start = frame.start + (err + TICK_JITTER)
|
|
end
|
|
frame.max, frame.rejected = period, nil
|
|
return true
|
|
end
|
|
|
|
-- two rejected gains one period apart are the real clock: relock to it
|
|
local periodic = frame.rejected and math.abs(now - frame.rejected - period) <= TICK_TOLERANCE
|
|
if not periodic then
|
|
frame.rejected = now
|
|
return false
|
|
end
|
|
end
|
|
|
|
frame.start, frame.max, frame.rejected = now, period, nil
|
|
return true
|
|
end
|
|
|
|
local energytick = CreateFrame("Frame", nil, pfUI.uf.player.power.bar)
|
|
energytick:SetAllPoints(pfUI.uf.player.power.bar)
|
|
energytick:RegisterEvent("PLAYER_ENTERING_WORLD")
|
|
energytick:RegisterUnitEvent("UNIT_DISPLAYPOWER", "player")
|
|
energytick:RegisterUnitEvent("UNIT_ENERGY", "player")
|
|
energytick:RegisterUnitEvent("UNIT_MANA", "player")
|
|
energytick:RegisterUnitEvent("UNIT_SPELLCAST_SUCCEEDED", "player")
|
|
energytick:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_STOP", "player")
|
|
energytick:RegisterEvent("SPELLS_CHANGED")
|
|
energytick:RegisterEvent("PLAYER_AURAS_CHANGED")
|
|
|
|
energytick:SetScript("OnEvent", function()
|
|
if UnitPowerType("player") == Enum.PowerType.Mana and C.unitframes.player.manatick == "1" then
|
|
this.mode = "MANA"
|
|
this:Show()
|
|
elseif UnitPowerType("player") == Enum.PowerType.Energy and C.unitframes.player.energy == "1" then
|
|
this.mode = "ENERGY"
|
|
this:Show()
|
|
else
|
|
this:Hide()
|
|
end
|
|
|
|
if event == "SPELLS_CHANGED" or event == "PLAYER_AURAS_CHANGED" then
|
|
energyRegenTimeMod = nil
|
|
return
|
|
end
|
|
|
|
if event == "PLAYER_ENTERING_WORLD" then
|
|
this.lastPower = UnitPower("player")
|
|
end
|
|
|
|
-- the rule arms on the cast (Spell::TakePower: mana powerType, cost > 0),
|
|
-- not on a mana drop -- Mana Burn lowers mana without arming it
|
|
if event == "UNIT_SPELLCAST_SUCCEEDED" and arg1 == "player" then
|
|
local cost = C_Spell.GetSpellPowerCost(arg3)
|
|
cost = cost and cost[1]
|
|
if cost and cost.type == Enum.PowerType.Mana and cost.cost > 0 then
|
|
this.fsrSpell, this.fsrEnd = arg3, GetTime() + FIVE_SECOND_RULE
|
|
this.fsrGain = nil
|
|
end
|
|
return
|
|
end
|
|
|
|
-- Unit::Update won't expire the rule while the spending spell still channels
|
|
if event == "UNIT_SPELLCAST_CHANNEL_STOP" and arg1 == "player" then
|
|
if this.fsrSpell and this.fsrSpell == arg3 then
|
|
this.fsrEnd, this.fsrGain = GetTime() + FIVE_SECOND_RULE, nil
|
|
end
|
|
return
|
|
end
|
|
|
|
if (event == "UNIT_MANA" or event == "UNIT_ENERGY") and arg1 == "player" then
|
|
local power = UnitPower("player")
|
|
local diff = this.lastPower and (power - this.lastPower) or 0
|
|
this.lastPower = power
|
|
|
|
-- only a gain can be the tick; a spend never touches the phase
|
|
if diff > 0 and lockTick(this) then
|
|
-- a tick inside the window proves regen continues through it
|
|
if this.fsrEnd and this.fsrEnd > GetTime() then
|
|
this.fsrGain = true
|
|
end
|
|
end
|
|
|
|
-- phase is kept while hidden; OnUpdate catches up by whole periods
|
|
if this.mode == "MANA" and power >= UnitPowerMax("player") then
|
|
this:Hide()
|
|
end
|
|
end
|
|
end)
|
|
|
|
energytick:SetScript("OnUpdate", function()
|
|
-- Throttle for performance
|
|
if (this.tick or 0) > GetTime() then
|
|
return
|
|
end
|
|
this.tick = GetTime() + 0.020 -- ~50 FPS
|
|
|
|
-- five-second rule drains to nothing
|
|
local remaining = this.fsrEnd and (this.fsrEnd - GetTime()) or 0
|
|
if this.mode == "MANA" and remaining > 0 then
|
|
this.fsrbar:SetWidth(getBarWidth() * remaining / FIVE_SECOND_RULE)
|
|
this.fsrbar:Show()
|
|
else
|
|
this.fsrSpell, this.fsrEnd, this.fsrGain = nil, nil, nil
|
|
this.fsrbar:Hide()
|
|
end
|
|
|
|
if not this.start then
|
|
this.spark:SetAlpha(0)
|
|
return
|
|
end
|
|
|
|
if this.mode == "MANA" and UnitPower("player") >= UnitPowerMax("player") then
|
|
this.spark:SetAlpha(0)
|
|
return
|
|
end
|
|
|
|
this.current = GetTime() - this.start
|
|
|
|
-- roll over by whole periods, not from now: restarting bakes frame
|
|
-- overshoot into the phase as drift the lock then has to chase
|
|
if this.current > this.max then
|
|
this.start = this.start + this.max * math.floor(this.current / this.max)
|
|
this.max = getAdjustedTickTimer()
|
|
this.current = GetTime() - this.start
|
|
end
|
|
|
|
-- dim while the rule is up and nothing has ticked inside it yet
|
|
this.spark:SetAlpha((remaining > 0 and not this.fsrGain) and .4 or 1)
|
|
|
|
if not C.unitframes.player.pheight then
|
|
return
|
|
end
|
|
|
|
local pos = getBarWidth() * (this.current / this.max)
|
|
this.spark:SetPoint("LEFT", pos - ((C.unitframes.player.pheight + 5) / 2), 0)
|
|
end)
|
|
|
|
energytick.fsrbar = energytick:CreateTexture(nil, "ARTWORK")
|
|
energytick.fsrbar:SetTexture(1, 1, 1, .15)
|
|
energytick.fsrbar:SetPoint("TOPLEFT", 0, 0)
|
|
energytick.fsrbar:SetPoint("BOTTOMLEFT", 0, 0)
|
|
energytick.fsrbar:Hide()
|
|
|
|
energytick.spark = energytick:CreateTexture(nil, "OVERLAY")
|
|
energytick.spark:SetTexture("Interface\\CastingBar\\UI-CastingBar-Spark")
|
|
energytick.spark:SetHeight(C.unitframes.player.pheight + 15)
|
|
energytick.spark:SetWidth(C.unitframes.player.pheight + 5)
|
|
energytick.spark:SetBlendMode("ADD")
|
|
|
|
local hookUpdateConfig = pfUI.uf.player.UpdateConfig
|
|
function pfUI.uf.player.UpdateConfig()
|
|
energytick.spark:SetHeight(C.unitframes.player.pheight + 15)
|
|
energytick.spark:SetWidth(C.unitframes.player.pheight + 5)
|
|
hookUpdateConfig(pfUI.uf.player)
|
|
end
|
|
end)
|