- the four extra action bars become per-character options, off by default, on a new Action Bars options page — the toggles ride the server's own byte (`CMSG_SET_ACTIONBAR_TOGGLES` into `PLAYER_FIELD_BYTES`), so a fresh character's zero is the default for free; turning the bottom-left bar off gives the stance bar its previously unbuilt unraised look, end-caps and 64×64 rings - bag windows stop drawing straight through a raised bottom bar: the bottom band has ONE owner — the manage pass — the bag stack joins it through a listener seam, and a guard test walks all eight raisable-bar combinations across 24 bar/tenant pairs and refuses overlap - the item-push card takes the reference's real geometry, closing the gap 0887 measured - resizable addon windows stop at their bounds instead of pinning and walking across the screen: four per-frame f32s with 0.0 the disable sentinel, the clamp on the interactive drag only with the residual rebated out of the drag delta, `<ResizeBounds>` in XML honoured — and `SetFontString`'s three raises land exactly as the bytes have them Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
478 lines
18 KiB
Rust
478 lines
18 KiB
Rust
//! The cast bar's lifecycle against the transcribed reference behavior (decision 0137 phase 1):
|
||
//! the extracted 1.12 `CastingBarFrame.lua` is the spec — orange fill that tracks the clock, green
|
||
//! completion flash + fade, red Failed with the 1 s hold, and the channel bar counting *down*.
|
||
//!
|
||
//! Two halves, and the second one is load-bearing. The state tests read the Lua back
|
||
//! (`GetValue`/`GetStatusBarColor`/`GetAlpha`), which is blind to what actually *paints* — the
|
||
//! original transcription dropped the reference's two `CastingBarFlash:Hide()` calls and every state
|
||
//! test still passed while a full-brightness additive bloom sat over the bar for the whole cast. The
|
||
//! [`draw list`](UiScript::extract) tests below are the ones that see it.
|
||
|
||
use benilla_ui::script::{ExtractedQuad, QuadContent, ScriptValue, UiScript};
|
||
|
||
/// Load one shipped `assets/ui/<file>`, panicking on any loader error (the bag/panel tests' loader).
|
||
fn load_xml(s: &UiScript, file: &str) {
|
||
let text = std::fs::read_to_string(
|
||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||
.join("assets/ui")
|
||
.join(file),
|
||
)
|
||
.unwrap();
|
||
let doc = benilla_ui::framexml::parse(&text).unwrap();
|
||
let report = benilla_ui::loader::load(s, &doc, &|_| None);
|
||
assert!(
|
||
report.errors.is_empty(),
|
||
"{file}: loader errors: {:?}",
|
||
report.errors
|
||
);
|
||
}
|
||
|
||
fn harness() -> UiScript {
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
load_xml(&s, "Fonts.xml");
|
||
load_xml(&s, "CastingBar.xml");
|
||
s
|
||
}
|
||
|
||
fn bar_value(s: &UiScript) -> f64 {
|
||
s.eval::<f64>("return CastingBarFrame:GetValue()").unwrap()
|
||
}
|
||
|
||
fn bar_color(s: &UiScript) -> (f64, f64, f64) {
|
||
s.eval::<(f64, f64, f64)>("local r, g, b = CastingBarFrame:GetStatusBarColor(); return r, g, b")
|
||
.unwrap()
|
||
}
|
||
|
||
/// One tick of the app's real order (`drive_script`): OnUpdate, resolve, then the draw list.
|
||
fn frame(s: &mut UiScript, dt: f32) -> Vec<ExtractedQuad> {
|
||
s.tick(dt);
|
||
s.resolve();
|
||
s.extract()
|
||
}
|
||
|
||
/// The quad drawn from `Interface\...\<leaf>`, if any — texture regions keyed by their art.
|
||
fn tex_quad<'a>(quads: &'a [ExtractedQuad], leaf: &str) -> Option<&'a ExtractedQuad> {
|
||
quads.iter().find(|q| match &q.content {
|
||
QuadContent::Texture { path: Some(p), .. } => p.ends_with(leaf),
|
||
_ => false,
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn cast_fills_then_completes_green_and_fades() {
|
||
let mut s = harness();
|
||
assert!(
|
||
!s.eval::<bool>("return CastingBarFrame:IsShown()").unwrap(),
|
||
"hidden at load"
|
||
);
|
||
|
||
// SPELLCAST_START(name, ms): shown, orange, named, anchored to the clock.
|
||
s.fire_event(
|
||
"SPELLCAST_START",
|
||
vec![ScriptValue::Str("Frostbolt".into()), ScriptValue::Int(3000)],
|
||
);
|
||
assert!(s.eval::<bool>("return CastingBarFrame:IsShown()").unwrap());
|
||
assert_eq!(
|
||
s.eval::<String>("return CastingBarText:GetText()").unwrap(),
|
||
"Frostbolt"
|
||
);
|
||
let (r, g, b) = bar_color(&s);
|
||
assert!(
|
||
(r - 1.0).abs() < 1e-6 && (g - 0.7).abs() < 1e-6 && b.abs() < 1e-6,
|
||
"casting is orange (got {r} {g} {b})"
|
||
);
|
||
|
||
// The fill tracks GetTime: 1.5 s in (15 ticks) the value sits mid-window.
|
||
let start = bar_value(&s);
|
||
for _ in 0..15 {
|
||
s.tick(0.1);
|
||
}
|
||
let mid = bar_value(&s);
|
||
assert!(
|
||
(mid - start - 1.5).abs() < 0.05,
|
||
"1.5s of ticks advance the fill by 1.5 (got {})",
|
||
mid - start
|
||
);
|
||
|
||
// Completion: green, full, then the fade takes it below full alpha and eventually hides.
|
||
s.fire_event("SPELLCAST_STOP", vec![]);
|
||
assert_eq!(bar_color(&s), (0.0, 1.0, 0.0), "completed is green");
|
||
for _ in 0..30 {
|
||
s.tick(0.1);
|
||
}
|
||
assert!(
|
||
!s.eval::<bool>("return CastingBarFrame:IsShown()").unwrap(),
|
||
"the flash+fade ends hidden"
|
||
);
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
}
|
||
|
||
#[test]
|
||
fn a_hit_pushes_the_bar_back_it_does_not_cancel() {
|
||
// Pushback (`SMSG_SPELL_DELAYED` → `SPELLCAST_DELAYED`): a hit while casting must slide the
|
||
// bar's window out (the spark jumps back and it keeps running), never hide or fail it —
|
||
// decision 0256's open item, the "disappears on a hit" report.
|
||
let mut s = harness();
|
||
s.fire_event(
|
||
"SPELLCAST_START",
|
||
vec![ScriptValue::Str("Fireball".into()), ScriptValue::Int(3000)],
|
||
);
|
||
// 1.5 s in — half-way through a 3 s cast.
|
||
for _ in 0..15 {
|
||
s.tick(0.1);
|
||
}
|
||
let minmax = "local a, b = CastingBarFrame:GetMinMaxValues(); return a, b";
|
||
let (_, max_before) = s.eval::<(f64, f64)>(minmax).unwrap();
|
||
let remaining_before = max_before - bar_value(&s);
|
||
assert!(
|
||
(remaining_before - 1.5).abs() < 0.05,
|
||
"half-way: ~1.5 s left (got {remaining_before})"
|
||
);
|
||
|
||
// The hit: a 0.5 s pushback.
|
||
s.fire_event("SPELLCAST_DELAYED", vec![ScriptValue::Int(500)]);
|
||
assert!(
|
||
s.eval::<bool>("return CastingBarFrame:IsShown()").unwrap(),
|
||
"a hit never hides the bar"
|
||
);
|
||
let (r, g, b) = bar_color(&s);
|
||
assert!(
|
||
(r - 1.0).abs() < 1e-6 && (g - 0.7).abs() < 1e-6 && b.abs() < 1e-6,
|
||
"still orange — not failed/red (got {r} {g} {b})"
|
||
);
|
||
let (_, max_after) = s.eval::<(f64, f64)>(minmax).unwrap();
|
||
assert!(
|
||
(max_after - max_before - 0.5).abs() < 1e-6,
|
||
"the window end moved out by the 0.5 s pushback"
|
||
);
|
||
let remaining_after = max_after - bar_value(&s);
|
||
assert!(
|
||
(remaining_after - remaining_before - 0.5).abs() < 0.05,
|
||
"the cast now has ~0.5 s more to run (spark jumped back): {remaining_before} -> {remaining_after}"
|
||
);
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
}
|
||
|
||
#[test]
|
||
fn failed_cast_turns_red_holds_then_fades() {
|
||
let mut s = harness();
|
||
s.fire_event(
|
||
"SPELLCAST_START",
|
||
vec![ScriptValue::Str("Frostbolt".into()), ScriptValue::Int(3000)],
|
||
);
|
||
s.fire_event("SPELLCAST_FAILED", vec![]);
|
||
assert_eq!(bar_color(&s), (1.0, 0.0, 0.0), "failed is red");
|
||
assert_eq!(
|
||
s.eval::<String>("return CastingBarText:GetText()").unwrap(),
|
||
"Failed"
|
||
);
|
||
|
||
// The 1 s hold: still fully opaque half a second in…
|
||
for _ in 0..5 {
|
||
s.tick(0.1);
|
||
}
|
||
assert_eq!(
|
||
s.eval::<f64>("return CastingBarFrame:GetAlpha()").unwrap(),
|
||
1.0,
|
||
"holds at full alpha inside CASTING_BAR_HOLD_TIME"
|
||
);
|
||
// …then fades out and hides.
|
||
for _ in 0..30 {
|
||
s.tick(0.1);
|
||
}
|
||
assert!(
|
||
!s.eval::<bool>("return CastingBarFrame:IsShown()").unwrap(),
|
||
"after the hold the fade ends hidden"
|
||
);
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
}
|
||
|
||
#[test]
|
||
fn channel_counts_down_not_up() {
|
||
let mut s = harness();
|
||
// SPELLCAST_CHANNEL_START(ms, name) — args reversed vs START, per the reference contract.
|
||
s.fire_event(
|
||
"SPELLCAST_CHANNEL_START",
|
||
vec![
|
||
ScriptValue::Int(6000),
|
||
ScriptValue::Str("Starshards".into()),
|
||
],
|
||
);
|
||
assert!(s.eval::<bool>("return CastingBarFrame:IsShown()").unwrap());
|
||
assert_eq!(
|
||
s.eval::<String>("return CastingBarText:GetText()").unwrap(),
|
||
"Starshards"
|
||
);
|
||
|
||
let full = bar_value(&s);
|
||
for _ in 0..10 {
|
||
s.tick(0.1);
|
||
}
|
||
let after_1s = bar_value(&s);
|
||
assert!(
|
||
(full - after_1s - 1.0).abs() < 0.05,
|
||
"a channel drains: 1s of ticks take the value DOWN by 1 (got {})",
|
||
full - after_1s
|
||
);
|
||
|
||
// The server's mid-channel correction re-anchors the window (pushback shortens it).
|
||
s.fire_event("SPELLCAST_CHANNEL_UPDATE", vec![ScriptValue::Int(2000)]);
|
||
s.tick(0.1);
|
||
let corrected = bar_value(&s);
|
||
assert!(
|
||
corrected < after_1s,
|
||
"an update to 2s-left pulls the fill further down ({corrected} < {after_1s})"
|
||
);
|
||
|
||
s.fire_event("SPELLCAST_CHANNEL_STOP", vec![]);
|
||
assert_eq!(bar_color(&s), (0.0, 1.0, 0.0), "channel end flashes green");
|
||
for _ in 0..30 {
|
||
s.tick(0.1);
|
||
}
|
||
assert!(
|
||
!s.eval::<bool>("return CastingBarFrame:IsShown()").unwrap(),
|
||
"ends hidden"
|
||
);
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
}
|
||
|
||
// ── The draw list ────────────────────────────────────────────────────────────────────────────────
|
||
|
||
const FLASH: &str = "UI-CastingBar-Flash";
|
||
const SPARK: &str = "UI-CastingBar-Spark";
|
||
const BORDER: &str = "UI-CastingBar-Border";
|
||
const FILL: &str = "UI-StatusBar";
|
||
|
||
/// The completion bloom is an `alphaMode="ADD"` texture the size of the whole frame. The reference
|
||
/// keeps it hidden for the entire cast (`CastingBarFlash:Hide()` on every OnUpdate) and only shows
|
||
/// it, from alpha 0, once the cast lands. Dropping those two calls painted a full-brightness white
|
||
/// smear over the bar from the first frame — invisible to every state assertion above.
|
||
#[test]
|
||
fn the_flash_stays_hidden_for_the_whole_cast() {
|
||
let mut s = harness();
|
||
s.fire_event(
|
||
"SPELLCAST_START",
|
||
vec![ScriptValue::Str("Frostbolt".into()), ScriptValue::Int(3000)],
|
||
);
|
||
for i in 0..25 {
|
||
let quads = frame(&mut s, 0.1);
|
||
assert!(
|
||
tex_quad(&quads, FLASH).is_none(),
|
||
"tick {i}: the flash must not draw during a cast"
|
||
);
|
||
assert!(
|
||
tex_quad(&quads, SPARK).is_some(),
|
||
"tick {i}: the spark rides the fill's leading edge"
|
||
);
|
||
}
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
}
|
||
|
||
/// On completion the flash appears at alpha 0 and ramps by CASTING_BAR_FLASH_STEP (0.2) per
|
||
/// REFERENCE tick — the ref steps per rendered frame, and decision 0454 normalizes our steps to
|
||
/// its 30 Hz tick (`arg1 × CASTING_BAR_REF_TICK`), so a 1/30 s update advances exactly one
|
||
/// reference step and the tail is wall-clock stable at any render rate. The spark goes away.
|
||
/// The frame is still fully opaque through the ramp — the fade only starts once the flash has
|
||
/// finished.
|
||
#[test]
|
||
fn the_flash_blooms_from_zero_only_on_completion() {
|
||
const REF_TICK: f32 = 1.0 / 30.0; // one reference tick of wall clock
|
||
let mut s = harness();
|
||
s.fire_event(
|
||
"SPELLCAST_START",
|
||
vec![ScriptValue::Str("Frostbolt".into()), ScriptValue::Int(3000)],
|
||
);
|
||
frame(&mut s, 0.1);
|
||
s.fire_event("SPELLCAST_STOP", vec![]);
|
||
|
||
// The event alone shows it at 0; each update adds a step. (The frame is opaque until the ramp
|
||
// completes, so the quad's alpha IS the flash's own.)
|
||
for (i, expected) in [0.2f32, 0.4, 0.6, 0.8, 1.0].into_iter().enumerate() {
|
||
let quads = frame(&mut s, REF_TICK);
|
||
let flash = tex_quad(&quads, FLASH).expect("the flash draws after completion");
|
||
assert!(
|
||
(flash.alpha - expected).abs() < 1e-5,
|
||
"ramp step {i}: flash alpha {} != {expected}",
|
||
flash.alpha
|
||
);
|
||
assert!(
|
||
tex_quad(&quads, SPARK).is_none(),
|
||
"ramp step {i}: the spark is hidden once the cast lands"
|
||
);
|
||
}
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
}
|
||
|
||
/// A failed cast never flashes — the reference only arms the ramp on STOP/CHANNEL_STOP.
|
||
#[test]
|
||
fn a_failed_cast_never_flashes() {
|
||
let mut s = harness();
|
||
s.fire_event(
|
||
"SPELLCAST_START",
|
||
vec![ScriptValue::Str("Frostbolt".into()), ScriptValue::Int(3000)],
|
||
);
|
||
frame(&mut s, 0.1);
|
||
s.fire_event("SPELLCAST_FAILED", vec![]);
|
||
for i in 0..25 {
|
||
let quads = frame(&mut s, 0.1);
|
||
assert!(
|
||
tex_quad(&quads, FLASH).is_none(),
|
||
"tick {i}: a failed cast must not bloom"
|
||
);
|
||
}
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
}
|
||
|
||
/// `drawLayer="BORDER"` on the `<StatusBar>`: the orange fill draws *under* the frame art, so the
|
||
/// border's bevel is never painted over. (Omitting the attribute defaults the fill to ARTWORK, the
|
||
/// border art's own layer, where declaration order put the fill on top.)
|
||
#[test]
|
||
fn the_fill_draws_beneath_the_border_art() {
|
||
let mut s = harness();
|
||
s.fire_event(
|
||
"SPELLCAST_START",
|
||
vec![ScriptValue::Str("Frostbolt".into()), ScriptValue::Int(3000)],
|
||
);
|
||
let quads = frame(&mut s, 0.1);
|
||
let fill = tex_quad(&quads, FILL).expect("fill");
|
||
let border = tex_quad(&quads, BORDER).expect("border");
|
||
assert!(
|
||
fill.z < border.z,
|
||
"the fill (z {}) draws before the border art (z {})",
|
||
fill.z,
|
||
border.z
|
||
);
|
||
}
|
||
|
||
/// A StatusBar fill CROPs its texture — it never squeezes it (wow-re `nameplate-vkey.md`). At a
|
||
/// fraction f the quad is f·width wide AND samples u ∈ [0, f], so `UI-StatusBar`'s left-to-right
|
||
/// ramp keeps its true gradient at every fill level.
|
||
#[test]
|
||
fn the_fill_crops_its_texture_rather_than_stretching_it() {
|
||
let mut s = harness();
|
||
s.fire_event(
|
||
"SPELLCAST_START",
|
||
vec![ScriptValue::Str("Frostbolt".into()), ScriptValue::Int(4000)],
|
||
);
|
||
// 1.0 s into a 4 s cast: a quarter filled.
|
||
for _ in 0..10 {
|
||
frame(&mut s, 0.1);
|
||
}
|
||
let quads = frame(&mut s, 0.0);
|
||
let fill = tex_quad(&quads, FILL).expect("fill");
|
||
let QuadContent::Texture { tex_coords, .. } = &fill.content else {
|
||
panic!("fill is a texture")
|
||
};
|
||
let [l, r, t, b] = tex_coords
|
||
.expect("a bar fill always carries its crop")
|
||
.edges();
|
||
let rect = fill.rect.expect("fill rect");
|
||
let frac = rect.width() / 195.0;
|
||
assert!((frac - 0.25).abs() < 0.02, "a quarter filled (got {frac})");
|
||
assert!(
|
||
(l - 0.0).abs() < 1e-6 && (t - 0.0).abs() < 1e-6 && (b - 1.0).abs() < 1e-6,
|
||
"a horizontal bar crops only the u axis (got {l} {r} {t} {b})"
|
||
);
|
||
assert!(
|
||
(r - frac).abs() < 1e-5,
|
||
"u1 tracks the fill fraction: {r} != {frac}"
|
||
);
|
||
}
|
||
|
||
/// The resolved bottom edge of a named frame, in UI units (y-up from the screen bottom).
|
||
fn bottom(s: &UiScript, name: &str) -> f64 {
|
||
s.eval::<f64>(&format!("return {name}:GetBottom()"))
|
||
.unwrap()
|
||
}
|
||
|
||
/// The managed bottom-stack positions (decision 0272): the ref's UIParent.lua re-anchors the
|
||
/// cast bar and the chat window over whatever bottom bars are showing — the XML anchors
|
||
/// (55 / 85) are only pre-manage defaults. The bar visibilities are the mechanism's only
|
||
/// inputs, so plain Lua stubs exercising IsShown() stand in for the real always-on multibars
|
||
/// (0270) and the stance bar; the arithmetic asserted is the ref table's own
|
||
/// (base + bottomEither/bottomLeft + pet, and chat's bottomLeft-and-pet +23 extra).
|
||
#[test]
|
||
fn managed_positions_track_the_bottom_bar_stack() {
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
load_xml(&s, "Fonts.xml");
|
||
load_xml(&s, "UIParent.xml");
|
||
load_xml(&s, "CastingBar.xml");
|
||
load_xml(&s, "ChatFrame.xml");
|
||
|
||
// The loader's post-load bootstrap, replayed with no bars in existence: the bare bases.
|
||
s.run("UIParent_ManageFramePositions()").unwrap();
|
||
s.resolve();
|
||
assert_eq!(
|
||
bottom(&s, "CastingBarFrame"),
|
||
60.0,
|
||
"baseY replaces the XML 55"
|
||
);
|
||
assert_eq!(bottom(&s, "ChatFrame1"), 85.0, "chat baseY");
|
||
|
||
// The always-on bottom multibars appear (0270): bottomEither for the bar, bottomLeft for chat.
|
||
// The bar stubs carry a no-op SetPoint: `MultiBarBottomLeft` and `ShapeshiftBarFrame` are
|
||
// themselves rows in UIPARENT_MANAGED_FRAME_POSITIONS, so since those frames wear their
|
||
// reference names the pass positions them as well as reading their visibility.
|
||
s.run("MultiBarBottomLeft = { IsShown = function() return true end, SetPoint = function() end, ClearAllPoints = function() end }; MultiBarBottomRight = MultiBarBottomLeft; UIParent_ManageFramePositions()")
|
||
.unwrap();
|
||
s.resolve();
|
||
assert_eq!(bottom(&s, "CastingBarFrame"), 100.0, "60 + bottomEither 40");
|
||
assert_eq!(bottom(&s, "ChatFrame1"), 102.0, "85 + bottomLeft 17");
|
||
|
||
// The stance bar shows (the warrior at login): the pet term, plus chat's both-flags extra.
|
||
s.run("ShapeshiftBarFrame = { IsShown = function() return true end, SetPoint = function() end, ClearAllPoints = function() end }; UIParent_ManageFramePositions()")
|
||
.unwrap();
|
||
s.resolve();
|
||
assert_eq!(bottom(&s, "CastingBarFrame"), 140.0, "60 + 40 + pet 40");
|
||
assert_eq!(
|
||
bottom(&s, "ChatFrame1"),
|
||
142.0,
|
||
"85 + 17 + pet 17 + both-flags 23"
|
||
);
|
||
|
||
// It hides again (a druid leaving forms is the live case): everything settles back.
|
||
s.run("ShapeshiftBarFrame = { IsShown = function() return false end, SetPoint = function() end, ClearAllPoints = function() end }; UIParent_ManageFramePositions()")
|
||
.unwrap();
|
||
s.resolve();
|
||
assert_eq!(bottom(&s, "CastingBarFrame"), 100.0);
|
||
assert_eq!(bottom(&s, "ChatFrame1"), 102.0);
|
||
}
|
||
|
||
/// **`CastingBarFrameStatusBar` is published, and it IS `CastingBarFrame`.**
|
||
///
|
||
/// `CastingBarFrame.lua:16` ends `CastingBarFrame_OnLoad` with `CastingBarFrameStatusBar =
|
||
/// CastingBarFrame` — a plain alias, because in 1.12 the casting bar is itself the StatusBar and
|
||
/// there is no separate child. Our transcription copied the eight `RegisterEvent`s and both field
|
||
/// inits and dropped that one line.
|
||
///
|
||
/// It cost 11 corpus addons, the largest single name in the session-start `attempt to index global`
|
||
/// row, and implementing it moved survivors 74 -> 85.
|
||
///
|
||
/// **This is the second time this file has been caught dropping lines from the same function** (see
|
||
/// the module doc: the original transcription also lost two `CastingBarFlash:Hide()` calls). A line
|
||
/// missing from INSIDE an otherwise-verbatim function is invisible to every instrument here —
|
||
/// `framexml-transcription-diff.py` compares elements and handlers, not bodies.
|
||
///
|
||
/// Asserted as identity, not just non-nil: an alias that pointed at some other frame would satisfy
|
||
/// "exists" and still break every addon that drives the bar through it.
|
||
#[test]
|
||
fn the_casting_bar_publishes_its_status_bar_alias() {
|
||
let s = harness();
|
||
assert!(
|
||
s.eval::<bool>("return CastingBarFrameStatusBar ~= nil")
|
||
.unwrap(),
|
||
"the reference publishes this name in CastingBarFrame_OnLoad"
|
||
);
|
||
assert!(
|
||
s.eval::<bool>("return CastingBarFrameStatusBar == CastingBarFrame")
|
||
.unwrap(),
|
||
"the alias IS the bar — 1.12 has no separate child StatusBar"
|
||
);
|
||
// And it drives: the addons that index it call StatusBar methods straight through.
|
||
s.run("CastingBarFrameStatusBar:SetMinMaxValues(0, 10) CastingBarFrameStatusBar:SetValue(4)")
|
||
.unwrap();
|
||
assert!((bar_value(&s) - 4.0).abs() < 1e-9);
|
||
}
|