Files
benilla-octo/crates/benilla-app/assets/ui/CastingBar.xml
T
SamandClaude Fable 5 d1140829d7 the interface code is ours: the transcription debt is paid
- forty files that carried the reference's literal Lua are rewritten as our own expression of the behaviour — worst identical run 61 lines down to 7 — and the README now says exactly that
- the overlap gate measures the code itself across all four reference corpora, and ratchets: the number may only go down
- a lost session ends at the login screen instead of silently re-authenticating and stealing the account back
- a building's skybox is a real model: Caverns of Time's white ceiling becomes its painted nebula and additive stars

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 19:21:03 +07:00

295 lines
14 KiB
XML

<Ui>
<!--
The cast bar — benilla's own implementation of the 1.12 CastingBarFrame contract. The
extracted reference (Interface\FrameXML\CastingBarFrame.xml/.lua, mpqx; decision 0137
phase 1) is the SPECIFICATION: geometry, textures, colors, and every timing constant are
read off it, and the observable lifecycle below is the one it defines. The code is ours.
Driven by the SPELLCAST_* events crate::ui_cast fires off the wire (START name,ms / STOP /
FAILED / INTERRUPTED / DELAYED / CHANNEL_START ms,name / CHANNEL_UPDATE ms / CHANNEL_STOP).
The reference's globals (CastingBarText/Spark/Flash) are named regions, so they publish as
globals here exactly as they do there; `this` is the firing frame (RF-0025), and every
handler below reads it once into a named local rather than threading the global through a
dozen method calls. Deliberate departures from the specification's own .lua: its dead code
(the `if not this:IsVisible() then this:Hide() end` no-op guard at the head of the STOP
branch; the two write-only `this.mode` stores nothing reads), and the flash/fade steps
normalized to the reference tick rate (the CASTING_BAR_REF_TICK comment below —
decision 0454).
SPELLCAST_DELAYED is live: SMSG_SPELL_DELAYED (pushback on taking a hit) is decoded and fed,
so a hit slides the bar's window out instead of letting it finish early (decision 0256).
POSITION: the XML anchor y=55 below is only the pre-manage default, exactly as in the
reference — UIParent.lua overrides it on every bottom-stack layout pass
(UIPARENT_MANAGED_FRAME_POSITIONS["CastingBarFrame"]: baseY 60, +40 per either bottom
multibar, +40 per pet/stance bar, +9 per rep bar). Our transcription of that mechanism
lives in UIParent.xml (decision 0272); with the always-on multibars the bar sits at
y=100, and y=140 while the stance bar shows.
-->
<Script><![CDATA[
CASTING_BAR_ALPHA_STEP = 0.05
CASTING_BAR_FLASH_STEP = 0.2
CASTING_BAR_HOLD_TIME = 1
-- The ref applies the two steps PER OnUpdate TICK (no arg1 scaling) — the flash/fade
-- tail is frame-rate dependent by the reference's own design: ~25 ticks from hold-expiry
-- to hidden, i.e. 0.83 s at the reference bottle's configured cap (WoW/WTF/Config.wtf
-- `SET maxfps "30"`), 0.42 s at an era-typical 60 fps, a blink at 120. We render fast,
-- so the step lines below are normalized to the reference tick rate — the bar's
-- hold+fade then matches the reference window at any render rate (decision 0454).
CASTING_BAR_REF_TICK = 30
-- The 1.12 GlobalStrings values (no GlobalStrings.lua is loaded yet).
FAILED = FAILED or "Failed"
INTERRUPTED = INTERRUPTED or "Interrupted"
-- THE BAR'S WHOLE VOCABULARY, one entry per wire event, each taking the frame. OnLoad
-- registers this table's keys and OnEvent looks the incoming event up in it, so the set
-- the bar LISTENS for and the set it can ANSWER are the same set by construction — there
-- is no second list to fall out of step with. (The reference spells both halves out: eight
-- RegisterEvent calls, then a sixty-line if-chain testing the same eight strings back.)
local castingBarEvent = {}
-- Seat the fill at `value` and ride the spark on its leading edge. The bar is 195px wide,
-- so a fraction of `span` scales straight to a pixel offset from the frame's LEFT. A cast
-- and a channel differ only in which value and which span they hand in.
local function castingBarTrack(bar, value, span)
bar:SetValue(value)
CastingBarFlash:Hide()
local sparkPosition = ((value - bar.startTime) / span) * 195
-- Pushback slides startTime PAST the wall clock, so a cast really can sit before its
-- own window's start for a frame or two. (A channel cannot — its value counts down
-- from the far end and stops at startTime — so here the clamp is dead weight rather
-- than a change of behaviour.)
if sparkPosition < 0 then
sparkPosition = 0
end
CastingBarSpark:SetPoint("CENTER", CastingBarFrame, "LEFT", sparkPosition, 2)
end
-- A cast opens: orange, a full window anchored on the clock, the spark at the left edge.
castingBarEvent.SPELLCAST_START = function(bar)
bar:SetStatusBarColor(1.0, 0.7, 0.0)
CastingBarSpark:Show()
bar.startTime = GetTime()
bar.maxValue = bar.startTime + (arg2 / 1000)
bar:SetMinMaxValues(bar.startTime, bar.maxValue)
bar:SetValue(bar.startTime)
CastingBarText:SetText(arg1)
bar:SetAlpha(1.0)
bar.holdTime = 0
bar.casting = 1
bar.fadeOut = nil
bar:Show()
end
-- A channel opens. Its args are START's the other way round — milliseconds first, then the
-- name — and its fill DRAINS, so the bar is seated at the far end and walks back.
castingBarEvent.SPELLCAST_CHANNEL_START = function(bar)
bar:SetStatusBarColor(1.0, 0.7, 0.0)
CastingBarSpark:Show()
bar.maxValue = 1
bar.startTime = GetTime()
bar.endTime = bar.startTime + (arg1 / 1000)
bar.duration = arg1 / 1000
bar:SetMinMaxValues(bar.startTime, bar.endTime)
bar:SetValue(bar.endTime)
CastingBarText:SetText(arg2)
bar:SetAlpha(1.0)
bar.holdTime = 0
bar.casting = nil
bar.channeling = 1
bar.fadeOut = nil
bar:Show()
end
-- The server's mid-channel correction: the window keeps its LENGTH and is re-anchored to
-- end arg1 ms from now, so a pushback eats into what is left rather than extending it.
castingBarEvent.SPELLCAST_CHANNEL_UPDATE = function(bar)
if not bar:IsShown() then
return
end
local span = bar.endTime - bar.startTime
bar.endTime = GetTime() + (arg1 / 1000)
bar.startTime = bar.endTime - span
bar:SetMinMaxValues(bar.startTime, bar.endTime)
end
-- Pushback on taking a hit (SMSG_SPELL_DELAYED, decision 0256): BOTH ends of the window
-- slide out by arg1 ms, so the cast has that much longer to run and the spark jumps back.
-- A hit never cancels the bar.
castingBarEvent.SPELLCAST_DELAYED = function(bar)
if not bar:IsShown() then
return
end
local pushback = arg1 / 1000
bar.startTime = bar.startTime + pushback
bar.maxValue = bar.maxValue + pushback
bar:SetMinMaxValues(bar.startTime, bar.maxValue)
end
-- The cast landed, or a channel ran its course: green, full, and the completion bloom
-- armed at alpha 0 for OnUpdate to ramp. `flag` names the one field the event clears,
-- which is the only thing that separates the two of them.
local function castingBarFinished(bar, flag)
if not bar:IsShown() then
return
end
bar:SetValue(bar.maxValue)
bar:SetStatusBarColor(0.0, 1.0, 0.0)
CastingBarSpark:Hide()
CastingBarFlash:SetAlpha(0.0)
CastingBarFlash:Show()
bar[flag] = nil
bar.flash = 1
bar.fadeOut = 1
end
castingBarEvent.SPELLCAST_STOP = function(bar) castingBarFinished(bar, "casting") end
castingBarEvent.SPELLCAST_CHANNEL_STOP = function(bar) castingBarFinished(bar, "channeling") end
-- The cast did NOT land: red, the reason where the spell's name was, and held opaque for
-- CASTING_BAR_HOLD_TIME before the fade starts — no completion bloom. A channel is never
-- ended this way (the server closes one with CHANNEL_STOP), so a live channel ignores both.
local function castingBarAborted(bar, reason)
if not bar:IsShown() or bar.channeling then
return
end
bar:SetValue(bar.maxValue)
bar:SetStatusBarColor(1.0, 0.0, 0.0)
CastingBarSpark:Hide()
CastingBarText:SetText(reason)
bar.casting = nil
bar.fadeOut = 1
bar.holdTime = GetTime() + CASTING_BAR_HOLD_TIME
end
castingBarEvent.SPELLCAST_FAILED = function(bar) castingBarAborted(bar, FAILED) end
castingBarEvent.SPELLCAST_INTERRUPTED = function(bar) castingBarAborted(bar, INTERRUPTED) end
function CastingBarFrame_OnLoad()
local bar = this
for spellcast in pairs(castingBarEvent) do
bar:RegisterEvent(spellcast)
end
bar.casting = nil
bar.holdTime = 0
-- The alias global the reference publishes at the end of its own OnLoad
-- (`CastingBarFrame.lua:16`). `CastingBarFrame` IS the StatusBar in 1.12 — there is no
-- separate child frame — so the name an addon reaches for has to resolve to the frame
-- itself.
--
-- 11 corpus addons index `CastingBarFrameStatusBar`, and it was the largest single
-- name in the whole session-start `attempt to index global` row. It went missing here
-- once and nothing warned: a line dropped from INSIDE a function is invisible to every
-- instrument we have. `framexml-transcription-diff.py` compares elements and handlers,
-- not function bodies, and would not even pair these two files (ours is CastingBar.xml,
-- the reference's is CastingBarFrame.xml).
CastingBarFrameStatusBar = bar
bar:Hide()
end
function CastingBarFrame_OnEvent()
local handle = castingBarEvent[event]
if handle then
handle(this)
end
end
function CastingBarFrame_OnUpdate()
local bar = this
if bar.casting then
-- The fill IS the wall clock, pinned at the window's end once the cast is spent.
local now = GetTime()
if now > bar.maxValue then
now = bar.maxValue
end
castingBarTrack(bar, now, bar.maxValue - bar.startTime)
elseif bar.channeling then
local now = GetTime()
if now >= bar.endTime then
bar.channeling = nil
bar.fadeOut = 1
return
end
-- A channel drains: the value is the window's start plus what is LEFT of it, so
-- the fill empties from full instead of filling from empty.
castingBarTrack(bar, bar.startTime + (bar.endTime - now), bar.endTime - bar.startTime)
elseif GetTime() < bar.holdTime then
return
elseif bar.flash then
local alpha = CastingBarFlash:GetAlpha() + CASTING_BAR_FLASH_STEP * (arg1 * CASTING_BAR_REF_TICK)
if alpha < 1 then
CastingBarFlash:SetAlpha(alpha)
else
CastingBarFlash:SetAlpha(1.0)
bar.flash = nil
end
elseif bar.fadeOut then
local alpha = bar:GetAlpha() - CASTING_BAR_ALPHA_STEP * (arg1 * CASTING_BAR_REF_TICK)
if alpha > 0 then
bar:SetAlpha(alpha)
else
bar.fadeOut = nil
bar:Hide()
end
end
end
]]></Script>
<StatusBar name="CastingBarFrame" drawLayer="BORDER" toplevel="true" parent="UIParent" hidden="true">
<Size><AbsDimension x="195" y="13"/></Size>
<Anchors>
<Anchor point="BOTTOM">
<Offset><AbsDimension x="0" y="55"/></Offset>
</Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture setAllPoints="true">
<Color r="0" g="0" b="0" a="0.5"/>
</Texture>
</Layer>
<Layer level="ARTWORK">
<FontString name="CastingBarText" inherits="GameFontHighlight">
<Size><AbsDimension x="185" y="16"/></Size>
<Anchors>
<Anchor point="TOP">
<Offset><AbsDimension x="0" y="5"/></Offset>
</Anchor>
</Anchors>
</FontString>
<Texture name="CastingBarBorder" file="Interface\CastingBar\UI-CastingBar-Border">
<Size><AbsDimension x="256" y="64"/></Size>
<Anchors>
<Anchor point="TOP">
<Offset><AbsDimension x="0" y="28"/></Offset>
</Anchor>
</Anchors>
</Texture>
</Layer>
<Layer level="OVERLAY">
<Texture name="CastingBarSpark" file="Interface\CastingBar\UI-CastingBar-Spark" alphaMode="ADD">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="CENTER"/>
</Anchors>
</Texture>
<Texture name="CastingBarFlash" file="Interface\CastingBar\UI-CastingBar-Flash" alphaMode="ADD">
<Size><AbsDimension x="256" y="64"/></Size>
<Anchors>
<Anchor point="TOP">
<Offset><AbsDimension x="0" y="28"/></Offset>
</Anchor>
</Anchors>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnLoad>CastingBarFrame_OnLoad()</OnLoad>
<OnEvent>CastingBarFrame_OnEvent()</OnEvent>
<OnUpdate>CastingBarFrame_OnUpdate()</OnUpdate>
</Scripts>
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
<BarColor r="1.0" g="0.7" b="0.0"/>
</StatusBar>
</Ui>