- a size-less `<Model>` pane's rect is its file's box in layout units: the reference's geometry overrides (`0x76d080`/`0x76d0d0`) answer the bbox extent whenever no size is authored, `bboxExtent · 768·√(a²+1) · layoutScale` FrameXML units (`1280 · extent` at 4:3), written into the pane's layout input when the file's facts land, re-derived on an aspect change, and ended for good by any authored size; `SetPosition` is the same unit, which is how `0x4a7b20` centres the arrow at `½·GetWidth, ½·GetHeight` through the override — so the map arrow's sprite arm and its hand-sized 33.6 px footprint go (its rect is `0.0262 × 0.0263` layout units, 33.5 × 33.7 FrameXML units at 4:3, 41.0 × 41.2 at 16:9), and the stock `ItemAnim` pane plays the item push unadapted (`ReplaceIconTexture` on the type-14 batch, the 1000 ms clamp, the stock `OnAnimFinished`), `ContainerFrameAdapters.xml` losing 0887's Lua card and its hand-measured 42.41 × 124.72 rect — the implicit one at 16:9 — and keeping the keyring fork and the three bag verbs (2015); and the cooldown is the reference's own model: the texture-transform law `uv' = R_q((uv + t − p) ⊙ s) + p`, `p = (½, ½)`, the raw quaternion component-lerped and never normalised, is one function (`uv_transform`) with the shader's UV fold as its twin — rotation and scale tracks bake per file sequence slot beside the translation, the mat-anim table gains an affine row `[cos − 1, sin, sx − 1, sy − 1]` on `anim_slots.z` whose identity is the zero row so every static batch reads exactly what it did, and a UI tile clones the twin material per animated batch and writes both rows every frame off the pane's play head, never the world clock; the stock `Cooldown.xml` runs off the chain on 2007's two handlers, the file's four quadrant quads turned by their own rotation tracks (`0 → −90°` over each quadrant's 250 ms window, clockwise from 12 o'clock) and its alpha tracks revealing the star for the flash; retired: the native `Cooldown` widget and `SetCooldown` (an Era API 1.12 never had — `CreateFrame("Cooldown")` is the reference's unknown-type refusal now), the loader's `Model → Cooldown` substitution, the app's hand-built pie and star, and our `Cooldown.xml`; `assets/ui` is eight files (2019)
- `UpdateMapHighlight` gets its zone-level exits (B360): at zone zoom ours returned the nil tail for every cursor position, so hovering Deadwind Pass from Duskwood showed no label where 1.12 shows "Deadwind Pass"; the reference's `0x4a7fa0` runs two lookups at zone level and answers a NAME only — first the current zone's exploration-gated overlay list (the builder at `0x4a6ad9` gating on the explored byte), each row's HitRect scaled by 1/1002 and 1/668 against the normalized cursor, edges inclusive, the first hit naming its Area1, then the continent grid re-windowed through the displayed zone's own rect, naming the neighbouring zone or city and nothing for the displayed zone itself — and only the name-only tail is reachable from there, fileName nil and the six coords zero, so the stock frame hides the highlight quad, as the director's own 1.12 shot shows; and `SetVerticalScroll` stores the offset as given (B370, B363): the reference's `0x786db0` compares the new offset against the old and writes it to `[+0x328]` verbatim, `0x787100` re-anchors the scroll child off it, and the range at `[+0x320]` is never read on that path — every clamp the reference exhibits is FrameXML's, through the scroll bar's `[min, max]`; ours clamped into `[0, GetVerticalScrollRange()]`, the child's overflow past the FRAME, which is 40 px short of where `FauxScrollFrame_Update` sends the bar on the 220-tall skills list (three rows lost) and 15 px short on the 287-tall who list (one); the setter fires `OnVerticalScroll` only on a change, as the bytes do, `BenillaScroll_Step` bounds its wheel in Lua where the reference's handler does, and the ScrollFrame kind's prose stops calling its offset unpinned (2017)
- overdraw's cheap half, the sky first: since 0588 every sky element wrote `frag_depth = 0.0`, which costs a pipeline its early-Z, so on an outdoor frame every full-screen dome fragment under the ground and the buildings was shaded in full (the gradient's `asin`, `atan2` and eight table lookups at dawn and dusk) and thrown away; one shared `sky_vertex.wgsl` — bevy's mesh vertex verbatim plus `out.position.z = 0.0` — is now the vertex stage of the gradient dome, the cloud dome, the star patches and the celestial discs and glares, and the model lane's `WOW_SKY_DEPTH` branch moves from its fragment to its vertex, so no sky fragment declares `frag_depth` and the rasterizer rejects the covered screen before the shader runs; every sky `specialize` zeroes the rasterizer bias constant the `sky_order` rung had been leaking into `DepthBiasState::constant`, harmless only while the fragment overwrote its depth, and the prose beside the pin follows it; then the retained `static_gx` pass draws BEFORE bevy's opaque pass (`StartMainPass → static_gx → MainOpaquePass`) — buildings and doodads are the frame's best occluders and a terrain fragment its dearest, six samples a pixel — so walls reject the terrain behind them instead of the reverse, the pass taking the first-use clear with it and the dev depth probe ordered after it; measured on 16 capture scenarios: 14 bit-identical, `northshire-dusk` 1 px and `water-night` 11 px, attributed to exact depth ties on static/terrain silhouettes and last-bit interpolation noise; a depth prepass and near-to-far bins are declined until a journal says where the time goes (2016)
- the grass cut goes per pixel because of a quarter-mip: `0x6813d0` sets `D3DSAMP_MIPMAPLODBIAS = +0.25` on stage 0 (`0x6813f4`) once per frame before the detail-doodad pass — capture-confirmed on all 29 doodad batches, and outside the `[0x6b1000, 0x6b3200)` scan 2004 had called complete; it decides the whole look because the `clutter_cut` probe finds 6 of 31 shipped detail atlases carry a BINARY alpha pyramid (mip 0 has DXT3's 16 levels, every level below exactly two, both starting zones among them), so `a × ramp` degenerates to a global on/off and every texel of a leaf crosses at the same depth — Elwynn's survivor coverage is flat at 47.6 % across the whole 52.5–61.1 yd band, then 0.0 %, while the probe's box-filtered control erodes 47.7 → 20.9 over the same band, so the art has the gradient the shipped pyramid threw away, and `clutter_size` puts the median tuft at 0.92 yd tall, 1.03 yd deep, a full yard for the boundary plane to sweep; the escape is bilinear: full alpha needs a 2×2 all-opaque texel neighbourhood, and those run out between mip 2 and 3 (179/56/10/0/0/0 on the dominant Elwynn cell), so the reference's λ ≈ 2.9–3.3 sits past the cliff and the tuft erodes continuously — 2.2 % of a tuft's pixels leave in the last half-yard with the bias, 11.6 % without, 24.2 % one mip sharper; wgpu has no sampler LOD bias, so the clutter lane re-samples base colour at `view.mip_bias + 0.25`, keeping the sampler every other model batch shares at the reference's 0; ruled out on the way: our BLP decoder (all nine uploaded levels byte-identical against the archive's 52 793 BLP2s), mip regeneration, the `has_mipmaps` byte's value 2 (a plain boolean, an authoring marker), anisotropy and geometry; what it does not fix is resolution-dependent, and so is the reference's own look — λ with the bias is 3.22 at the capture's 648p but 2.48 at 1080p and 1.48 at 2160p, so at a modern resolution the reference itself pops leaf by leaf, and scaling the bias with the viewport is one reversible line left for a look call (2021)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
451 lines
18 KiB
Rust
451 lines
18 KiB
Rust
//! The stock `Interface\FrameXML\MainMenuBarMicroButtons.xml` — the eight micro buttons in the
|
||
//! main bar's right-hand recess — off the player's chain behind the stock bar (their anchor
|
||
//! target `MainMenuBarArtFrame`) into a bare engine (decision 1987).
|
||
//!
|
||
//! What these guard, in order: the row's geometry (29×58 at art-frame BOTTOMLEFT +(552,2), a
|
||
//! 26 px stride, and the reference's own 1 px nudge once the talent gate has run); the
|
||
//! `HitRectInsets` header that keeps a button's transparent top from eating the mouse; the
|
||
//! character button's portrait crop; `UpdateTalentButton`'s level-10 gate closing the row up
|
||
//! behind the hidden talent button; the two-line hover with the bound key in its label; and,
|
||
//! under the whole shipped manifest, `UpdateMicroButtons` following a real panel and the
|
||
//! level-up pulse the reference hangs on the talent button.
|
||
|
||
use benilla_ui::script::{QuadContent, ScriptValue, TexCoords, UiScript, UnitState};
|
||
|
||
/// The eight, in row order.
|
||
const ROW: [&str; 8] = [
|
||
"CharacterMicroButton",
|
||
"SpellbookMicroButton",
|
||
"TalentMicroButton",
|
||
"QuestLogMicroButton",
|
||
"SocialsMicroButton",
|
||
"WorldMapMicroButton",
|
||
"MainMenuMicroButton",
|
||
"HelpMicroButton",
|
||
];
|
||
|
||
/// The stock bar, then the stock row, into a 1024×768 engine, asserting both load clean. 1024
|
||
/// wide is deliberate: the 1024-wide bar then centers at x=0, so every reference offset below is
|
||
/// also an absolute screen coordinate.
|
||
fn harness() -> UiScript {
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
for file in [
|
||
"Interface\\FrameXML\\Fonts.xml",
|
||
"Interface\\FrameXML\\Cooldown.xml",
|
||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||
r"Interface\FrameXML\UIParent.xml",
|
||
// The eight labels and their NEWBIE_TOOLTIP_* lines, read at each button's OnLoad…
|
||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||
// …through `TEXT()`.
|
||
"Interface\\FrameXML\\BasicControls.xml",
|
||
"Interface\\FrameXML\\MainMenuBar.xml",
|
||
r"Interface\FrameXML\MoneyFrame.lua",
|
||
r"Interface\FrameXML\MoneyFrame.xml",
|
||
"Interface\\FrameXML\\GameTooltip.xml",
|
||
"Interface\\FrameXML\\ActionBarFrame.xml",
|
||
"Interface\\FrameXML\\BonusActionBarFrame.xml",
|
||
r"Interface\FrameXML\MainMenuBarMicroButtons.xml",
|
||
] {
|
||
super::test_ui::load_ui(&s, file);
|
||
}
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
s
|
||
}
|
||
|
||
/// Set the player's level and fire the `UNIT_LEVEL` the talent gate listens for.
|
||
fn set_player_level(s: &mut UiScript, level: u32) {
|
||
s.set_unit(
|
||
"player",
|
||
Some(UnitState {
|
||
exists: true,
|
||
level,
|
||
..Default::default()
|
||
}),
|
||
);
|
||
s.fire_event("UNIT_LEVEL", vec![ScriptValue::Str("player".into())]);
|
||
}
|
||
|
||
fn left_of(s: &UiScript, name: &str) -> f64 {
|
||
s.eval::<f64>(&format!("return {name}:GetLeft()"))
|
||
.unwrap_or_else(|e| panic!("{name}: {e}"))
|
||
}
|
||
|
||
/// The row's geometry, the reference's own numbers: CharacterMicroButton at the art frame's
|
||
/// BOTTOMLEFT +(552,2), each button 29×58, the rest chained BOTTOMLEFT-to-previous-BOTTOMRIGHT
|
||
/// +(-3,0) ⇒ a 26 px stride, so the eight run x 552..757 as declared — between the page arrows
|
||
/// (x≈522) and the bag cluster at the far right. **As declared** is the operative phrase: the XML
|
||
/// seats the quest-log button at −3 and `UpdateTalentButton` re-seats it at −2
|
||
/// (`MainMenuBarMicroButtons.lua:139`), so the first level pass at 10+ nudges the tail right by
|
||
/// one pixel. A real row is 1 px wider past its first `PLAYER_ENTERING_WORLD` than its XML
|
||
/// says; that is the client's, and it is asserted here as the reference's law rather than
|
||
/// sanded off (our retired file wrote −3 both ways).
|
||
#[test]
|
||
fn the_micro_row_sits_where_the_reference_puts_it() {
|
||
let _data = benilla_formats::wow_data_or_skip!();
|
||
let mut s = harness();
|
||
s.resolve();
|
||
|
||
// As declared, before any level pass: a clean 26 px stride.
|
||
for (i, name) in ROW.iter().enumerate() {
|
||
let (left, bottom, w, h) = s
|
||
.eval::<(f64, f64, f64, f64)>(&format!(
|
||
"return {name}:GetLeft(), {name}:GetBottom(), {name}:GetWidth(), {name}:GetHeight()"
|
||
))
|
||
.unwrap_or_else(|e| panic!("{name}: {e}"));
|
||
assert_eq!((w, h), (29.0, 58.0), "{name} size");
|
||
assert_eq!(
|
||
left,
|
||
552.0 + 26.0 * i as f64,
|
||
"{name} left edge, as declared"
|
||
);
|
||
assert_eq!(bottom, 2.0, "{name} sits 2 above the bar's bottom");
|
||
}
|
||
|
||
// Past level 10 the gate re-seats the quest-log button at −2: the tail moves 1 px right.
|
||
set_player_level(&mut s, 60);
|
||
s.resolve();
|
||
for (i, name) in ROW.iter().enumerate().skip(3) {
|
||
assert_eq!(
|
||
left_of(&s, name),
|
||
553.0 + 26.0 * i as f64,
|
||
"{name} after the talent gate's own −2"
|
||
);
|
||
}
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
}
|
||
|
||
/// The `HitRectInsets top="18"` header: the micro-button art fills only the lower ~40 of the 58,
|
||
/// and the empty top must stay transparent to the mouse — otherwise the row eats hover over the
|
||
/// bar's XP strip and the sliver of world above it.
|
||
#[test]
|
||
fn the_transparent_top_of_a_micro_button_does_not_capture_the_mouse() {
|
||
let _data = benilla_formats::wow_data_or_skip!();
|
||
let mut s = harness();
|
||
s.resolve();
|
||
|
||
// Mid-button horizontally (552 + 14), inside the art band (y 2..42) ⇒ captures.
|
||
assert_eq!(
|
||
s.hit_test_name(566.0, 20.0).as_deref(),
|
||
Some("CharacterMicroButton"),
|
||
"the art band takes the mouse"
|
||
);
|
||
// Same column, in the dead 18-unit header (y 42..60) ⇒ nothing of the row captures it.
|
||
assert_ne!(
|
||
s.hit_test_name(566.0, 50.0).as_deref(),
|
||
Some("CharacterMicroButton"),
|
||
"the inset header must be transparent to the mouse"
|
||
);
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
}
|
||
|
||
/// The character button's face: `MicroButtonPortrait` binds the SAME `"player"` portrait slot
|
||
/// the unit frame samples, and carries the reference's crop window out to the renderer. Both
|
||
/// halves matter — the crop is what makes an 18×25 rectangle show a face instead of the whole
|
||
/// square bake squashed into it. Pushing the button swaps the window and dims it
|
||
/// (`CharacterMicroButton_SetPushed`, `MainMenuBarMicroButtons.lua:109-117`).
|
||
#[test]
|
||
fn the_character_button_carries_the_player_portrait_through_the_reference_crop() {
|
||
let _data = benilla_formats::wow_data_or_skip!();
|
||
let mut s = harness();
|
||
s.resolve();
|
||
|
||
let window = |s: &mut UiScript| {
|
||
s.extract()
|
||
.into_iter()
|
||
.find_map(|q| match q.content {
|
||
QuadContent::Texture {
|
||
portrait_unit: Some(unit),
|
||
tex_coords,
|
||
circular,
|
||
..
|
||
} => Some((unit, tex_coords, circular)),
|
||
_ => None,
|
||
})
|
||
.expect("the micro button's portrait quad")
|
||
};
|
||
|
||
let (unit, coords, circular) = window(&mut s);
|
||
assert_eq!(unit, "player");
|
||
assert!(
|
||
circular,
|
||
"the reference's round stencil lives in the bake's own UV space, so the crop below yields \
|
||
a rectangular slice OF a masked face — not an ellipse fitted to this 18x25 region"
|
||
);
|
||
let round4 = |c: TexCoords| match c {
|
||
TexCoords::Rect(e) => e.map(|v| (v * 10_000.0).round() / 10_000.0),
|
||
TexCoords::Corners(_) => panic!("the 4-edge form"),
|
||
};
|
||
assert_eq!(
|
||
round4(coords.expect("a crop window")),
|
||
[0.2, 0.8, 0.0666, 0.9],
|
||
"the normal window (CharacterMicroButton_SetNormal)"
|
||
);
|
||
|
||
s.run("CharacterMicroButton_SetPushed()").unwrap();
|
||
s.resolve();
|
||
let (_, coords, _) = window(&mut s);
|
||
assert_eq!(
|
||
round4(coords.expect("a crop window")),
|
||
[0.2666, 0.8666, 0.0, 0.8333],
|
||
"the held-down window (CharacterMicroButton_SetPushed)"
|
||
);
|
||
assert_eq!(
|
||
s.eval::<f64>("return MicroButtonPortrait:GetAlpha()")
|
||
.unwrap(),
|
||
0.5,
|
||
"…and the face dims while the button is down"
|
||
);
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
}
|
||
|
||
/// `UpdateTalentButton` (l.133-141): under level 10 the talent button is hidden and the quest
|
||
/// log button slides onto its seat, so the row has no hole in it; at 10 the button returns and
|
||
/// the tail shifts back out — by one stride less two, the gate's own −2.
|
||
#[test]
|
||
fn the_talent_button_appears_at_level_ten_and_the_row_closes_up_below_it() {
|
||
let _data = benilla_formats::wow_data_or_skip!();
|
||
let mut s = harness();
|
||
|
||
set_player_level(&mut s, 9);
|
||
s.resolve();
|
||
assert!(
|
||
!s.eval::<bool>("return TalentMicroButton:IsVisible()")
|
||
.unwrap(),
|
||
"no talents before 10"
|
||
);
|
||
// The quest log button takes the talent button's own seat — slot 3, not slot 4.
|
||
assert_eq!(left_of(&s, "QuestLogMicroButton"), 552.0 + 26.0 * 2.0);
|
||
assert_eq!(
|
||
left_of(&s, "HelpMicroButton"),
|
||
552.0 + 26.0 * 6.0,
|
||
"the whole tail moves up one slot with it"
|
||
);
|
||
|
||
set_player_level(&mut s, 10);
|
||
s.resolve();
|
||
assert!(
|
||
s.eval::<bool>("return TalentMicroButton:IsVisible()")
|
||
.unwrap(),
|
||
"the button returns at 10"
|
||
);
|
||
assert_eq!(
|
||
left_of(&s, "QuestLogMicroButton"),
|
||
553.0 + 26.0 * 3.0,
|
||
"…and the tail goes back out, at the gate's own −2"
|
||
);
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
}
|
||
|
||
/// The hover (the template's OnEnter, `MainMenuBarMicroButtons.xml:12-14`): every button's plate
|
||
/// is the reference's TWO-line `GameTooltip_AddNewbieTip` — the label, then that button's own
|
||
/// `NEWBIE_TOOLTIP_*` explanation in gold. 1.12 ships detailed tips ON (`SHOW_NEWBIE_TIPS = "1"`,
|
||
/// `UIOptionsFrame.lua:100`; ours sits in OptionsFrame.xml's uvar block), so the paragraph is the
|
||
/// DEFAULT hover (decision 0661). Through the engine's own hover, because the stock OnEnter reads
|
||
/// the firing frame off `this`. For the bound ones the label carries the key the way the
|
||
/// reference prints it — `GetBindingKey`'s raw token in `NORMAL_FONT_COLOR_CODE` parentheses
|
||
/// (`MicroButtonTooltipText`, l.11-18) — re-read on `UPDATE_BINDINGS`, which is the event
|
||
/// `bindings.rs` fires after every (re)load of the table.
|
||
#[test]
|
||
fn every_micro_button_hovers_with_its_reference_explanation() {
|
||
let _data = benilla_formats::wow_data_or_skip!();
|
||
let mut s = harness();
|
||
s.run("SHOW_NEWBIE_TIPS = \"1\"").unwrap();
|
||
// A key the character button's label can carry: the registry, then the reference's own
|
||
// `SetBinding`, then the event the row re-reads its labels on.
|
||
s.register_bindings(&crate::bindings::registry_commands());
|
||
assert_eq!(
|
||
s.eval::<Option<i64>>("return SetBinding(\"C\", \"TOGGLECHARACTER0\")")
|
||
.unwrap(),
|
||
Some(1)
|
||
);
|
||
s.fire_event("UPDATE_BINDINGS", vec![]);
|
||
s.resolve();
|
||
|
||
for (button, label, newbie) in [
|
||
(
|
||
"CharacterMicroButton",
|
||
"CHARACTER_BUTTON",
|
||
"NEWBIE_TOOLTIP_CHARACTER",
|
||
),
|
||
// The spellbook's own OnEnter (l.79-87) picks its label by `PlayerHasSpells()`.
|
||
(
|
||
"SpellbookMicroButton",
|
||
"PlayerHasSpells() and SPELLBOOK_ABILITIES_BUTTON or ABILITYBOOK_BUTTON",
|
||
"NEWBIE_TOOLTIP_SPELLBOOK",
|
||
),
|
||
(
|
||
"TalentMicroButton",
|
||
"TALENTS_BUTTON",
|
||
"NEWBIE_TOOLTIP_TALENTS",
|
||
),
|
||
(
|
||
"QuestLogMicroButton",
|
||
"QUESTLOG_BUTTON",
|
||
"NEWBIE_TOOLTIP_QUESTLOG",
|
||
),
|
||
(
|
||
"SocialsMicroButton",
|
||
"SOCIAL_BUTTON",
|
||
"NEWBIE_TOOLTIP_SOCIAL",
|
||
),
|
||
(
|
||
"WorldMapMicroButton",
|
||
"WORLDMAP_BUTTON",
|
||
"NEWBIE_TOOLTIP_WORLDMAP",
|
||
),
|
||
(
|
||
"MainMenuMicroButton",
|
||
"MAINMENU_BUTTON",
|
||
"NEWBIE_TOOLTIP_MAINMENU",
|
||
),
|
||
("HelpMicroButton", "HELP_BUTTON", "NEWBIE_TOOLTIP_HELP"),
|
||
] {
|
||
super::test_ui::hover(&mut s, button);
|
||
assert_eq!(
|
||
s.eval::<i64>("return GameTooltip:NumLines()").unwrap(),
|
||
2,
|
||
"{button}: the label, then the explanation"
|
||
);
|
||
let label = s.eval::<String>(&format!("return {label}")).unwrap();
|
||
let line1 = s
|
||
.eval::<String>("return GameTooltipTextLeft1:GetText()")
|
||
.unwrap();
|
||
assert!(
|
||
line1.starts_with(&label),
|
||
"{button}: line 1 is {line1:?}, expected it to open with {label:?}"
|
||
);
|
||
assert_eq!(
|
||
s.eval::<String>("return GameTooltipTextLeft2:GetText()")
|
||
.unwrap(),
|
||
s.eval::<String>(&format!("return {newbie}")).unwrap(),
|
||
"{button}: line 2 is the reference's {newbie}, verbatim"
|
||
);
|
||
assert_eq!(
|
||
s.eval::<i64>("return GameTooltip.default").unwrap(),
|
||
1,
|
||
"{button}: the default-corner anchor, not ANCHOR_RIGHT off the button"
|
||
);
|
||
super::test_ui::unhover(&mut s);
|
||
}
|
||
|
||
// The bound one carries its key the reference's way: the raw token, gold, in parentheses,
|
||
// after a space — `MicroButtonTooltipText`'s own concatenation.
|
||
super::test_ui::hover(&mut s, "CharacterMicroButton");
|
||
let line1 = s
|
||
.eval::<String>("return GameTooltipTextLeft1:GetText()")
|
||
.unwrap();
|
||
assert_eq!(
|
||
line1,
|
||
s.eval::<String>(
|
||
"return CHARACTER_BUTTON .. \" \" .. NORMAL_FONT_COLOR_CODE .. \"(C)\" .. FONT_COLOR_CODE_CLOSE"
|
||
)
|
||
.unwrap(),
|
||
"the key read on UPDATE_BINDINGS"
|
||
);
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
}
|
||
|
||
/// Under the whole shipped manifest — the only VM in which every panel `UpdateMicroButtons`
|
||
/// (l.20-84) reads exists — a button is PUSHED exactly while its panel is up, the character
|
||
/// button's face dims with it, and the level-up pulse the reference hangs on the talent button
|
||
/// (`TalentMicroButton_OnEvent`, l.119-124: sixty seconds, unless the character sheet is open)
|
||
/// is live — 0304's "no microbutton pulse" scope-out, retired by the file that owns the pulse.
|
||
#[test]
|
||
fn a_micro_button_pushes_while_its_panel_is_open_and_the_talent_button_pulses_on_a_ding() {
|
||
let _data = benilla_formats::wow_data_or_skip!();
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
// A player always exists by the time the manifest loads (1051/1848) — with a race and a
|
||
// class, which the sheet's own `PaperDollFrame_SetLevel` formats into its title on show.
|
||
s.set_unit(
|
||
"player",
|
||
Some(UnitState {
|
||
exists: true,
|
||
name: Some("Probefour".into()),
|
||
level: 60,
|
||
race: Some("Night Elf".into()),
|
||
race_file: Some("NightElf".into()),
|
||
class: Some("Warrior".into()),
|
||
class_file: Some("WARRIOR".into()),
|
||
sex: 2,
|
||
is_player: true,
|
||
player_controlled: true,
|
||
..Default::default()
|
||
}),
|
||
);
|
||
// The reference's strings, which the app runs ahead of the manifest (lifecycle.rs) — the
|
||
// ding's chat lines below format `SPELL_STAT0_NAME`..`4` through them.
|
||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
assert!(super::load_default_ui(&s).is_empty());
|
||
s.resolve();
|
||
|
||
let state = |s: &UiScript, button: &str| {
|
||
s.eval::<String>(&format!("return {button}:GetButtonState()"))
|
||
.unwrap()
|
||
};
|
||
let face = |s: &UiScript| {
|
||
s.eval::<f64>("return MicroButtonPortrait:GetAlpha()")
|
||
.unwrap()
|
||
};
|
||
|
||
assert_eq!(state(&s, "CharacterMicroButton"), "NORMAL");
|
||
assert_eq!(face(&s), 1.0);
|
||
// The button's own click (its OnClick, l.49-51) — the sheet's OnShow calls back.
|
||
s.run("ToggleCharacter(\"PaperDollFrame\")").unwrap();
|
||
assert_eq!(
|
||
state(&s, "CharacterMicroButton"),
|
||
"PUSHED",
|
||
"open sheet ⇒ button held down"
|
||
);
|
||
assert_eq!(
|
||
face(&s),
|
||
0.5,
|
||
"…and the face dims (CharacterMicroButton_SetPushed)"
|
||
);
|
||
s.run("ToggleCharacter(\"PaperDollFrame\")").unwrap();
|
||
assert_eq!(state(&s, "CharacterMicroButton"), "NORMAL");
|
||
assert_eq!(face(&s), 1.0);
|
||
|
||
// The reference's own full-screen panel; its OnShow/OnHide call back the same way.
|
||
s.run("ToggleWorldMap()").unwrap();
|
||
assert_eq!(state(&s, "WorldMapMicroButton"), "PUSHED");
|
||
s.run("ToggleWorldMap()").unwrap();
|
||
assert_eq!(state(&s, "WorldMapMicroButton"), "NORMAL");
|
||
|
||
// A ding with the sheet closed: sixty seconds on the pulse list (UiPanels.xml's
|
||
// SetButtonPulse, the reference's UIParent.lua machinery). The nine args are the ones
|
||
// `ui_unit` fires (level, health, power, talent points, five stats).
|
||
s.fire_event(
|
||
"PLAYER_LEVEL_UP",
|
||
vec![
|
||
ScriptValue::Int(61),
|
||
ScriptValue::Int(12),
|
||
ScriptValue::Int(0),
|
||
ScriptValue::Int(1),
|
||
ScriptValue::Int(1),
|
||
ScriptValue::Int(1),
|
||
ScriptValue::Int(1),
|
||
ScriptValue::Int(0),
|
||
ScriptValue::Int(0),
|
||
],
|
||
);
|
||
assert_eq!(
|
||
s.eval::<f64>("return TalentMicroButton.pulseTimeLeft")
|
||
.unwrap(),
|
||
60.0,
|
||
"the sixty-second pulse"
|
||
);
|
||
assert!(
|
||
s.eval::<bool>(
|
||
"for _, b in ipairs(PULSEBUTTONS) do if b == TalentMicroButton then return true end end \
|
||
return false"
|
||
)
|
||
.unwrap(),
|
||
"on the pulse list"
|
||
);
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
}
|