the cooldown goes stock, early-Z comes back, and lists reach the end
- 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>
This commit is contained in:
@@ -14,7 +14,7 @@ fn action_ids(s: &mut UiScript) -> Vec<u32> {
|
||||
fn shipped_action_bar_drives_end_to_end() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
super::test_ui::load_ui(&s, "Cooldown.xml");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\Cooldown.xml");
|
||||
// `load_ui` returns the same `report.frames` the disk reader asserted on, so this
|
||||
// count is the one that always stood here — moved, not re-derived.
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
@@ -180,7 +180,7 @@ fn shipped_action_bar_drives_end_to_end() {
|
||||
}
|
||||
|
||||
fn load_action_bar(s: &UiScript) {
|
||||
super::test_ui::load_ui(s, "Cooldown.xml");
|
||||
super::test_ui::load_ui(s, "Interface\\FrameXML\\Cooldown.xml");
|
||||
super::test_ui::load_ui(s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
super::test_ui::load_ui(s, "Interface\\FrameXML\\TextStatusBar.lua");
|
||||
super::test_ui::load_ui(s, "Interface\\FrameXML\\TextStatusBar.xml");
|
||||
@@ -251,17 +251,18 @@ fn state_feedback_drives_cooldown_checked_and_usable_through_the_xml() {
|
||||
}),
|
||||
);
|
||||
s.fire_event("ACTIONBAR_UPDATE_COOLDOWN", vec![]);
|
||||
// The stock machine (decision 2019): `CooldownFrame_SetTimer` arms sequence 0 and shows the
|
||||
// pane; the next paint's `OnUpdateModel` scrubs it to `(GetTime() − start) / duration`.
|
||||
super::test_ui::cooldown_facts(&mut s);
|
||||
s.tick(0.0);
|
||||
s.resolve();
|
||||
let sweep = s.extract().into_iter().find_map(|q| match q.content {
|
||||
QuadContent::Cooldown { fraction, flash } => Some((fraction, flash)),
|
||||
_ => None,
|
||||
});
|
||||
let (fraction, flash) = sweep.expect("the button's Cooldown widget is showing");
|
||||
assert!(
|
||||
(fraction - 0.4).abs() < 1e-3,
|
||||
"6 s of 10 s left ⇒ the sweep sits at 40%, got {fraction}"
|
||||
let play = super::test_ui::cooldown_play(&s, "ActionButton1Cooldown")
|
||||
.expect("the button's cooldown pane is showing, sequence 0 armed");
|
||||
assert_eq!(
|
||||
play,
|
||||
(0, 400),
|
||||
"6 s of 10 s left ⇒ the sweep sits at 40 %: sequence 0 at 400 ms"
|
||||
);
|
||||
assert_eq!(flash, None);
|
||||
|
||||
// The checked ring on the current action (the transcribed UpdateState).
|
||||
s.set_action_state(
|
||||
@@ -351,7 +352,11 @@ fn the_cooldown_sweep_paints_over_the_buttons_icon_and_ring() {
|
||||
&|c| matches!(c, QuadContent::Texture { path: Some(p), .. } if p.contains("UI-Quickslot2")),
|
||||
)
|
||||
.expect("the NormalTexture ring quad");
|
||||
let sweep = pos(&|c| matches!(c, QuadContent::Cooldown { .. })).expect("the sweep quad");
|
||||
let sweep = pos(&|c| {
|
||||
matches!(c, QuadContent::ModelPane { model: Some(m), .. }
|
||||
if m.eq_ignore_ascii_case(super::test_ui::COOLDOWN_MODEL))
|
||||
})
|
||||
.expect("the sweep pane's quad");
|
||||
assert!(
|
||||
icon < sweep,
|
||||
"the sweep (index {sweep}) must paint over the icon (index {icon})"
|
||||
@@ -829,7 +834,7 @@ fn shipped_bag_frame_drives_end_to_end() {
|
||||
let mut bar_frames = 0;
|
||||
for file in BAG_UI {
|
||||
let frames = load_ui(&s, file);
|
||||
if *file == "Cooldown.xml" {
|
||||
if *file == "Interface\\FrameXML\\Cooldown.xml" {
|
||||
load_ui(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_ui(&s, "Interface\\FrameXML\\TextStatusBar.lua");
|
||||
load_ui(&s, "Interface\\FrameXML\\TextStatusBar.xml");
|
||||
@@ -1351,7 +1356,7 @@ fn the_main_bar_pages_and_a_bonus_page_still_outranks_it() {
|
||||
// Fonts first: the pane's check-box labels colour from `RED_FONT_COLOR` in their own OnLoad.
|
||||
"Interface\\FrameXML\\Fonts.xml",
|
||||
r"Interface\FrameXML\UIParent.xml",
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
@@ -1517,7 +1522,7 @@ fn the_main_bar_pages_and_a_bonus_page_still_outranks_it() {
|
||||
fn bonus_bar_slides_up_with_sound_and_down_without() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
super::test_ui::load_ui(&s, "Cooldown.xml");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\Cooldown.xml");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\TextStatusBar.lua");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\TextStatusBar.xml");
|
||||
@@ -1739,7 +1744,7 @@ fn bonus_bar_slides_up_with_sound_and_down_without() {
|
||||
fn bonus_bar_turnaround_continues_from_position() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
super::test_ui::load_ui(&s, "Cooldown.xml");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\Cooldown.xml");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\TextStatusBar.lua");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\TextStatusBar.xml");
|
||||
@@ -1821,7 +1826,7 @@ fn bonus_bar_turnaround_continues_from_position() {
|
||||
fn the_page_arrows_do_not_steal_each_other_s_clicks() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
super::test_ui::load_ui(&s, "Cooldown.xml");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\Cooldown.xml");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\TextStatusBar.lua");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\TextStatusBar.xml");
|
||||
|
||||
@@ -1394,19 +1394,15 @@ fn bag_slot_cooldown_sweeps_through_the_xml() {
|
||||
s.set_container(0, Some(backpack(Some((52_000, 60_000, true)))));
|
||||
s.run("OpenAllBags()").unwrap();
|
||||
s.fire_event("BAG_UPDATE", vec![benilla_ui::script::ScriptValue::Int(0)]);
|
||||
// The stock machine (decision 2019): the slot's cooldown pane, sequence 0 scrubbed by the
|
||||
// next paint's `OnUpdateModel` to the elapsed fraction.
|
||||
super::test_ui::cooldown_facts(&mut s);
|
||||
s.tick(0.0);
|
||||
s.resolve();
|
||||
|
||||
let sweep = |s: &UiScript| {
|
||||
s.extract().iter().find_map(|q| match q.content {
|
||||
QuadContent::Cooldown { fraction, .. } => Some(fraction),
|
||||
_ => None,
|
||||
})
|
||||
};
|
||||
let fraction = sweep(&s).expect("the bag slot sweeps");
|
||||
assert!(
|
||||
(fraction - 0.8).abs() < 1e-3,
|
||||
"48 of 60 s elapsed: fraction {fraction}"
|
||||
);
|
||||
let sweep = |s: &UiScript| super::test_ui::cooldown_play_any(s);
|
||||
let play = sweep(&s).expect("the bag slot sweeps");
|
||||
assert_eq!(play, (0, 800), "48 of 60 s elapsed: sequence 0 at 800 ms");
|
||||
|
||||
// The cooldown clears (a CLEAR_COOLDOWN, or it simply ran out before the re-push): the
|
||||
// refresh event re-reads the now-cold triple and hides the widget.
|
||||
@@ -1835,16 +1831,16 @@ fn a_key_dropped_on_the_button_files_itself() {
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// The item-push drop animation (decision 0887): `ITEM_PUSH(container, icon)` runs the pushed item's
|
||||
/// icon down into **that** container's bag-bar button and nobody else's, along the curves read out of
|
||||
/// `ForcedBackpackItem.m2` — pop in, hang, fall, shrink away — and hides itself at the end.
|
||||
///
|
||||
/// This is the whole observable contract of `BenillaItemPushAnim_*`: the routing (which button), the
|
||||
/// motion (starts a fall above the button, lands centred on it), the fade/scale shape (the file's
|
||||
/// keys), and the CLAMP end (one play, then gone). It is also the regression net for the OnUpdate
|
||||
/// gate — an anim frame left shown would keep ticking forever.
|
||||
/// The item-push drop animation is the reference's own `<Model>` now (decisions 2013/2015):
|
||||
/// `ITEM_PUSH(container, icon)` reaches the stock `ItemAnim_OnEvent`, which puts the icon on the
|
||||
/// pane whose parent button owns that inventory slot (`ReplaceIconTexture`), arms sequence 0 at
|
||||
/// 0 and shows it; the pane's clock runs the file's 1000 ms clamp; `ItemAnim_OnAnimFinished`
|
||||
/// hides it. The pane authored no size, so its rect is the file's own bounding box in layout
|
||||
/// units — the 42.41 × 124.72 the 0887 card measured by hand at 16:9.
|
||||
#[test]
|
||||
fn an_item_push_drops_its_icon_into_the_bag_that_took_it() {
|
||||
use benilla_ui::widget::{ModelFileFacts, SequenceFacts};
|
||||
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1600.0, 900.0);
|
||||
@@ -1853,42 +1849,50 @@ fn an_item_push_drops_its_icon_into_the_bag_that_took_it() {
|
||||
}
|
||||
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
// `ForcedBackpackItem.m2` (`m2seq`/`m2batch`): one sequence, id 0, 1000 ms, clamp; the
|
||||
// header box spans the card's whole travel band — 0.02707 × 0.07962 model units, which at
|
||||
// 16:9 (a layout unit is 768·√(a²+1) = 1566.4 FrameXML units) is the 42.41 × 124.72 rect
|
||||
// 0887's card measured.
|
||||
const CARD: &str = r"Interface\ItemAnimations\ForcedBackpackItem.mdx";
|
||||
s.set_model_facts(
|
||||
CARD,
|
||||
ModelFileFacts {
|
||||
sequences: vec![SequenceFacts {
|
||||
anim_id: 0,
|
||||
duration_ms: 1000,
|
||||
looping: false,
|
||||
}],
|
||||
bbox: ([0.0, 0.0, 0.0], [0.02707, 0.07962, 0.0]),
|
||||
},
|
||||
);
|
||||
s.resolve();
|
||||
|
||||
let shown = |s: &UiScript, name: &str| {
|
||||
s.eval::<bool>(&format!("return {name}BenillaItemPush:IsShown()"))
|
||||
s.eval::<bool>(&format!("return {name}ItemAnim:IsShown()"))
|
||||
.unwrap()
|
||||
};
|
||||
// The card's centre relative to its button's centre, in screen px (y-up): (dx, dy).
|
||||
let offset = |s: &UiScript, name: &str| -> (f32, f32) {
|
||||
s.eval::<(f32, f32)>(&format!(
|
||||
"local a, b = {name}BenillaItemPush, {name} \
|
||||
local ax, ay = a:GetLeft() + a:GetWidth() / 2, a:GetBottom() + a:GetHeight() / 2 \
|
||||
local bx, by = b:GetLeft() + b:GetWidth() / 2, b:GetBottom() + b:GetHeight() / 2 \
|
||||
return ax - bx, ay - by"
|
||||
))
|
||||
.unwrap()
|
||||
};
|
||||
let size = |s: &UiScript, name: &str| {
|
||||
s.eval::<f32>(&format!("return {name}BenillaItemPush:GetWidth()"))
|
||||
.unwrap()
|
||||
};
|
||||
let alpha = |s: &UiScript, name: &str| {
|
||||
s.eval::<f32>(&format!("return {name}BenillaItemPush:GetAlpha()"))
|
||||
.unwrap()
|
||||
};
|
||||
// The card's centre relative to its button's BOTTOMRIGHT corner — the reference's OWN frame of
|
||||
// reference for this widget (it anchors the `<Model>`'s BOTTOMRIGHT to the button's), and the
|
||||
// only one the numbers are stable in: the card's placement depends on the anchor offset and
|
||||
// the M2 bounding box, NOT on how big the button is. Measuring against the button's centre
|
||||
// instead is what let 0887's placement look plausible.
|
||||
let corner = |s: &UiScript, name: &str| -> (f32, f32) {
|
||||
s.eval::<(f32, f32)>(&format!(
|
||||
"local a, b = {name}BenillaItemPush, {name} \
|
||||
local ax, ay = a:GetLeft() + a:GetWidth() / 2, a:GetBottom() + a:GetHeight() / 2 \
|
||||
return ax - b:GetRight(), ay - b:GetBottom()"
|
||||
))
|
||||
.unwrap()
|
||||
// The shown card panes, as the renderer sees them: `(icon, play head)` per visible pane.
|
||||
type Card = (Option<String>, Option<(u16, u32)>);
|
||||
let cards = |s: &mut UiScript| -> Vec<Card> {
|
||||
let heads = s.visible_model_panes();
|
||||
s.extract()
|
||||
.into_iter()
|
||||
.filter_map(|q| match q.content {
|
||||
QuadContent::ModelPane {
|
||||
handle,
|
||||
model: Some(m),
|
||||
icon,
|
||||
..
|
||||
} if m == CARD => Some((
|
||||
icon,
|
||||
heads
|
||||
.iter()
|
||||
.find(|p| p.handle == handle)
|
||||
.and_then(|p| p.play.map(|ph| (ph.anim_id, ph.cursor_ms))),
|
||||
)),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
// Nothing is animating until a push arrives.
|
||||
@@ -1899,11 +1903,11 @@ fn an_item_push_drops_its_icon_into_the_bag_that_took_it() {
|
||||
] {
|
||||
assert!(!shown(&s, b), "{b}'s card starts hidden");
|
||||
}
|
||||
assert!(cards(&mut s).is_empty());
|
||||
|
||||
// A push into equipped bag 2. `arg1` is the reference's own vocabulary since 1751 window 3 —
|
||||
// the button's INVENTORY-slot id, which is what `ItemAnim_OnEvent` compares against — so bag 2
|
||||
// is `CharacterBag1Slot`'s 21, not the container id 2 this used to send (`ui_loot.rs`
|
||||
// `push_container`, the `0x491bb5` selector).
|
||||
// is `CharacterBag1Slot`'s 21.
|
||||
s.fire_event(
|
||||
"ITEM_PUSH",
|
||||
vec![
|
||||
@@ -1911,159 +1915,70 @@ fn an_item_push_drops_its_icon_into_the_bag_that_took_it() {
|
||||
benilla_ui::script::ScriptValue::Str("Interface\\Icons\\INV_Misc_Bag_08".into()),
|
||||
],
|
||||
);
|
||||
assert!(
|
||||
shown(&s, "CharacterBag1Slot"),
|
||||
"the card that took it plays"
|
||||
);
|
||||
assert!(
|
||||
!shown(&s, "MainMenuBarBackpackButton") && !shown(&s, "KeyRingButton"),
|
||||
"…and nobody else's"
|
||||
);
|
||||
s.resolve();
|
||||
assert!(shown(&s, "CharacterBag1Slot"), "bag 2's card plays");
|
||||
for b in [
|
||||
"MainMenuBarBackpackButton",
|
||||
"CharacterBag0Slot",
|
||||
"KeyRingButton",
|
||||
] {
|
||||
assert!(!shown(&s, b), "{b} took nothing, so {b} animates nothing");
|
||||
}
|
||||
// The pushed icon reaches the RENDERER, not just the Lua state — exactly one quad carries it.
|
||||
let drawn = |s: &UiScript, icon: &str| {
|
||||
s.extract()
|
||||
.iter()
|
||||
.filter(
|
||||
|q| matches!(&q.content, QuadContent::Texture { path: Some(p), .. } if p == icon),
|
||||
)
|
||||
.count()
|
||||
};
|
||||
let live = cards(&mut s);
|
||||
assert_eq!(
|
||||
drawn(&s, "Interface\\Icons\\INV_Misc_Bag_08"),
|
||||
1,
|
||||
"the card wears the pushed item's icon (ITEM_PUSH's arg2)"
|
||||
live,
|
||||
vec![(
|
||||
Some(r"Interface\Icons\INV_Misc_Bag_08".to_string()),
|
||||
Some((0, 0))
|
||||
)],
|
||||
"one pane, the pushed icon on it, sequence 0 at 0"
|
||||
);
|
||||
// The implicit rect: the file's box in layout units, hung off the button's BOTTOMRIGHT
|
||||
// (−10, 0) as the stock template anchors it.
|
||||
let (w, h): (f32, f32) = s
|
||||
.eval("return CharacterBag1SlotItemAnim:GetWidth(), CharacterBag1SlotItemAnim:GetHeight()")
|
||||
.unwrap();
|
||||
assert!(
|
||||
(w - 42.41).abs() < 0.05 && (h - 124.72).abs() < 0.05,
|
||||
"the card's rect is the file's box: {w} × {h}"
|
||||
);
|
||||
let (dx, dy): (f32, f32) = s
|
||||
.eval(
|
||||
"local a, b = CharacterBag1SlotItemAnim, CharacterBag1Slot \
|
||||
return a:GetRight() - b:GetRight(), a:GetBottom() - b:GetBottom()",
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
(dx + 10.0).abs() < 0.01 && dy.abs() < 0.01,
|
||||
"BOTTOMRIGHT (−10, 0) off the button: ({dx}, {dy})"
|
||||
);
|
||||
|
||||
// Every figure below is the REFERENCE's, from wow-re's §5-cross-checked
|
||||
// `modelframe-implicit-size-law.md` §3 — not a ratio we chose. The three it publishes
|
||||
// (t = 0, 0.133, 1.0) are asserted to a tenth of a pixel; the rest assert the SHAPE of the
|
||||
// motion, which is where 0887's authored placement actually went wrong.
|
||||
//
|
||||
// What changed from 0887, and why the old numbers looked right: it measured against the
|
||||
// button's CENTRE and drove both axes off one shared 0..1 "drop" curve, which happens to land
|
||||
// within ~1 px of the truth in y for a 36 px button. In x it was wrong in DIRECTION — the card
|
||||
// drifts RIGHT as it falls, and the old code had it starting left and ending centred.
|
||||
|
||||
// t=0: invisible, full size, parked high above the button. The .m2's alpha key (0.000, 0.0),
|
||||
// scale key (0.000, 1.0), translation (0.000, (0,0)).
|
||||
let (cx, cy) = corner(&s, "CharacterBag1Slot");
|
||||
assert!(
|
||||
(cx + 30.14).abs() < 0.1 && (cy - 68.99).abs() < 0.1,
|
||||
"starts at the ref's (-30.14, 68.99) from the button's bottom-right: got ({cx}, {cy})"
|
||||
);
|
||||
assert!(
|
||||
(size(&s, "CharacterBag1Slot") - 36.86).abs() < 0.1,
|
||||
"the quad is 0.0288 model units, not the 0.03 that 0887 rounded it to: got {}",
|
||||
size(&s, "CharacterBag1Slot")
|
||||
);
|
||||
assert!(
|
||||
alpha(&s, "CharacterBag1Slot") < 0.01,
|
||||
"fades in from nothing"
|
||||
);
|
||||
|
||||
// t=0.133: fully faded in, at the 1.2x swell.
|
||||
s.tick(0.133);
|
||||
s.resolve();
|
||||
assert!(
|
||||
(alpha(&s, "CharacterBag1Slot") - 1.0).abs() < 0.02,
|
||||
"opaque by the alpha track's second key"
|
||||
);
|
||||
assert!(
|
||||
(size(&s, "CharacterBag1Slot") - 44.24).abs() < 0.1,
|
||||
"swollen to the ref's 44.24 px peak: got {}",
|
||||
size(&s, "CharacterBag1Slot")
|
||||
);
|
||||
// The pop moves the centre by a twentieth of a pixel, and that tiny drift is the tell that the
|
||||
// bone's PIVOT is not the quad's centre — scaling up pushes the centre away from the pivot,
|
||||
// scaling down pulls it in, one mechanism at both ends. Folding the pivot into the centre (the
|
||||
// obvious simplification) loses this and misses the landing point by a fifth of a pixel.
|
||||
let (px, py) = corner(&s, "CharacterBag1Slot");
|
||||
assert!(
|
||||
(px + 30.18).abs() < 0.02 && (py - 68.98).abs() < 0.02,
|
||||
"the 1.2x pop drifts the centre off the pivot by the ref's (-0.046, -0.015): got ({px}, {py})"
|
||||
);
|
||||
|
||||
// t=0.267: back to the icon's own size, still opaque, still parked.
|
||||
s.tick(0.134);
|
||||
s.resolve();
|
||||
assert!(
|
||||
(size(&s, "CharacterBag1Slot") - 36.86).abs() < 0.1,
|
||||
"settled back at the scale track's third key"
|
||||
);
|
||||
let (_, cy) = corner(&s, "CharacterBag1Slot");
|
||||
assert!(
|
||||
(cy - 68.99).abs() < 0.1,
|
||||
"has not started falling: got {cy}"
|
||||
);
|
||||
|
||||
// t=0.5: STILL PARKED — the translation track's second key is (0,0), so the whole first half
|
||||
// second is a hang. The other two tracks are already running down, though: the three curves
|
||||
// keep different schedules, which is the thing a "hold everything then drop" reading would get
|
||||
// wrong (scale/alpha are 0.687/0.682 here, on the 0.267→1.000 ramps).
|
||||
s.tick(0.233);
|
||||
s.resolve();
|
||||
let (_, cy) = corner(&s, "CharacterBag1Slot");
|
||||
assert!(
|
||||
(cy - 69.0).abs() < 0.1,
|
||||
"has not moved yet at the half second: got {cy}"
|
||||
);
|
||||
assert!(
|
||||
(size(&s, "CharacterBag1Slot") - 25.31).abs() < 0.2,
|
||||
"but is already dwindling: got {}",
|
||||
size(&s, "CharacterBag1Slot")
|
||||
);
|
||||
assert!(
|
||||
(alpha(&s, "CharacterBag1Slot") - 0.682).abs() < 0.02,
|
||||
"and already fading: got {}",
|
||||
alpha(&s, "CharacterBag1Slot")
|
||||
);
|
||||
|
||||
// t=0.75: halfway through the fall — and moving RIGHT as it drops, which is the axis 0887 had
|
||||
// backwards. The translation is per-axis (+0.0104, -0.0402 model units), not one shared curve.
|
||||
s.tick(0.25);
|
||||
s.resolve();
|
||||
let (cx, cy) = corner(&s, "CharacterBag1Slot");
|
||||
assert!(
|
||||
(cx + 23.34).abs() < 0.2,
|
||||
"drifted RIGHT by half the 13.3 px total: got {cx}"
|
||||
);
|
||||
assert!(
|
||||
(cy - 43.33).abs() < 0.2,
|
||||
"half of the 51.4 px fall travelled by t=0.75: got {cy}"
|
||||
);
|
||||
|
||||
// Just short of the end: down on the button, shrunk to nothing, faded out.
|
||||
s.tick(0.24);
|
||||
s.resolve();
|
||||
let (cx, cy) = corner(&s, "CharacterBag1Slot");
|
||||
assert!(
|
||||
(cx + 16.87).abs() < 0.2 && (cy - 18.68).abs() < 0.2,
|
||||
"arrives at the ref's landing point: got ({cx}, {cy})"
|
||||
);
|
||||
// Which is also, within a couple of pixels, the button's own centre — the reference's card
|
||||
// really does drop INTO the bag button, and that is the one thing 0887 got right for the wrong
|
||||
// reason. The corner-relative assertion above is the exact one; this is the sanity check, and
|
||||
// its tolerance is a button-size term, not slack: the landing point is fixed relative to the
|
||||
// button's BOTTOMRIGHT, so the offset from its CENTRE moves by half of any size change. The
|
||||
// bar's buttons are 37x37 now — `ItemButtonTemplate`'s own size, inherited through the
|
||||
// reference's `PaperDollItemSlotButtonTemplate` — where the deleted BagFrame.xml authored 36.
|
||||
let (dx, dy) = offset(&s, "CharacterBag1Slot");
|
||||
assert!(
|
||||
dx.abs() < 2.0 && dy.abs() < 2.0,
|
||||
"lands on the button it went into: got ({dx}, {dy})"
|
||||
);
|
||||
assert!(
|
||||
size(&s, "CharacterBag1Slot") < 2.0,
|
||||
"collapsed to the scale track's 0.0144"
|
||||
);
|
||||
assert!(alpha(&s, "CharacterBag1Slot") < 0.05, "faded out");
|
||||
|
||||
// Past 1.000s the CLAMP sequence is over — the ref's OnAnimFinished → Hide.
|
||||
s.tick(0.05);
|
||||
// The clock runs the file's clamp: half a second in, the play head is at 500 ms…
|
||||
s.tick(0.5);
|
||||
assert_eq!(cards(&mut s)[0].1, Some((0, 500)));
|
||||
// …and past 1000 ms the completion fires `ItemAnim_OnAnimFinished` → `Hide()`.
|
||||
s.tick(0.55);
|
||||
assert!(
|
||||
!shown(&s, "CharacterBag1Slot"),
|
||||
"one play, then gone (and no OnUpdate left running)"
|
||||
"one play, then gone (the stock OnAnimFinished hides it)"
|
||||
);
|
||||
assert!(cards(&mut s).is_empty());
|
||||
|
||||
// A second push RESTARTS the card (SetSequenceTime(0, 0) on a fresh arm).
|
||||
s.fire_event(
|
||||
"ITEM_PUSH",
|
||||
vec![
|
||||
benilla_ui::script::ScriptValue::Int(21),
|
||||
benilla_ui::script::ScriptValue::Str("Interface\\Icons\\INV_Misc_Bag_09".into()),
|
||||
],
|
||||
);
|
||||
s.resolve();
|
||||
assert_eq!(
|
||||
cards(&mut s),
|
||||
vec![(
|
||||
Some(r"Interface\Icons\INV_Misc_Bag_09".to_string()),
|
||||
Some((0, 0))
|
||||
)]
|
||||
);
|
||||
|
||||
// The keyring is a real destination, not a rounding of the backpack.
|
||||
|
||||
@@ -474,6 +474,19 @@ fn the_item_push_card_shares_the_band_with_a_raised_bar_exactly_as_the_reference
|
||||
);
|
||||
let failures = super::load_default_ui(&s);
|
||||
assert!(failures.is_empty(), "manifest load errors: {failures:#?}");
|
||||
// The card is the stock `<Model>` since 2015, sized by its file's box once the facts land
|
||||
// (`ForcedBackpackItem.m2`: one 1000 ms clamp; the box 0.02707 × 0.07962 model units).
|
||||
s.set_model_facts(
|
||||
r"Interface\ItemAnimations\ForcedBackpackItem.mdx",
|
||||
benilla_ui::widget::ModelFileFacts {
|
||||
sequences: vec![benilla_ui::widget::SequenceFacts {
|
||||
anim_id: 0,
|
||||
duration_ms: 1000,
|
||||
looping: false,
|
||||
}],
|
||||
bbox: ([0.0, 0.0, 0.0], [0.02707, 0.07962, 0.0]),
|
||||
},
|
||||
);
|
||||
s.run("MultiBarBottomLeft:Show() MultiBarBottomRight:Show() UIParent_ManageFramePositions()")
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
@@ -488,15 +501,23 @@ fn the_item_push_card_shares_the_band_with_a_raised_bar_exactly_as_the_reference
|
||||
s.tick(0.133); // the opaque peak — the instant the card is most visible
|
||||
s.resolve();
|
||||
assert!(
|
||||
shown(&s, "MainMenuBarBackpackButtonBenillaItemPush"),
|
||||
shown(&s, "MainMenuBarBackpackButtonItemAnim"),
|
||||
"the card plays"
|
||||
);
|
||||
|
||||
let card = rect(&s, "MainMenuBarBackpackButtonBenillaItemPush");
|
||||
// The pane's rect is the file's box in layout units at 16:9 — the card's whole travel band,
|
||||
// 42.41 × 124.72 hung off the button's BOTTOMRIGHT (−10, 0) — and the card the file's keys
|
||||
// put inside it at its opaque peak is the reference's 48.9..93.1 above the screen floor
|
||||
// (0887 measured that quad by hand; it sits inside this band).
|
||||
let card = rect(&s, "MainMenuBarBackpackButtonItemAnim");
|
||||
let bar = rect(&s, "MultiBarBottomRight");
|
||||
assert!(
|
||||
(card.1 - 48.9).abs() < 0.5 && (card.3 - 93.1).abs() < 0.5,
|
||||
"the card's band is the reference's 48.9..93.1 above the screen floor: got {card:?}"
|
||||
(card.2 - card.0 - 42.41).abs() < 0.05 && (card.3 - card.1 - 124.72).abs() < 0.05,
|
||||
"the pane's rect is the file's box: {card:?}"
|
||||
);
|
||||
assert!(
|
||||
card.1 <= 48.9 && card.3 >= 93.1,
|
||||
"the reference's card peak 48.9..93.1 lies inside the pane's band: {card:?}"
|
||||
);
|
||||
assert!(
|
||||
overlaps(bar, card),
|
||||
|
||||
@@ -42,7 +42,7 @@ fn harness() -> UiScript {
|
||||
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.lua");
|
||||
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.xml");
|
||||
|
||||
@@ -36,7 +36,7 @@ fn chat_ui() -> UiScript {
|
||||
"Interface\\FrameXML\\BasicControls.xml",
|
||||
// `FCF_ValidateChatFramePosition` (a tab-drag stop) reads `MainMenuBar:GetHeight()`; the
|
||||
// bar's own load-time chain precedes it, as in the action-bar harness.
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
|
||||
@@ -34,7 +34,7 @@ fn chat_frame() -> UiScript {
|
||||
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
|
||||
// `FCF_ValidateChatFramePosition` (a tab-drag stop) reads `MainMenuBar:GetHeight()`; the bar's
|
||||
// own load-time chain precedes it, as in the action-bar harness.
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.lua");
|
||||
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.xml");
|
||||
|
||||
@@ -72,7 +72,7 @@ fn load_room(s: &UiScript) {
|
||||
"Interface\\FrameXML\\BasicControls.xml",
|
||||
"Interface\\FrameXML\\StaticPopup.xml",
|
||||
"Interface\\FrameXML\\GameTooltip.xml",
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\Cooldown.xml",
|
||||
] {
|
||||
load_xml(s, file);
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ fn an_error_toast_draws_over_an_open_panel_window() {
|
||||
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIErrorsFrame.xml");
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Cooldown.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml"); // BenillaMoney_* — QuestLogDetail's reward money row
|
||||
// pane's UIPanelScrollFrameTemplate). A MISSING template is a loader *warning*, so an
|
||||
|
||||
@@ -22,7 +22,7 @@ fn exp_bar_harness() -> UiScript {
|
||||
"Interface\\FrameXML\\GameTooltip.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Interface\\FrameXML\\MainMenuBar.xml",
|
||||
|
||||
@@ -1,287 +0,0 @@
|
||||
//! The Cooldown widget's pixels (decision 0137 phase 4): the radial dark wipe + the finish
|
||||
//! flash, rebuilt natively from the byte-read `UI-Cooldown-Indicator.m2`. Split from [`super`]
|
||||
//! purely for size — [`cooldown_quads`] is the only face, called from the extract loop's
|
||||
//! `QuadContent::Cooldown` arm.
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
use crate::ui_pass::UiQuad;
|
||||
use benilla_assets::WorldAssets;
|
||||
|
||||
/// The cooldown wipe's paint: flat black at α = 0x99/255 — the byte content of
|
||||
/// `Interface\Cooldown\cooldown.blp` (a uniform 32² black texel field at alpha 0x99, with only a
|
||||
/// transparent clamp column for the sweep edge), constant through the sweep (the model's
|
||||
/// quadrant color-alpha tracks hold 1.0 for all of sequence 0 — the m2anim key dump).
|
||||
const COOLDOWN_WIPE_ALPHA: f32 = 153.0 / 255.0;
|
||||
|
||||
/// The finish-flash star's bone-scale pulse — sequence 1's 5 keys, byte-read off
|
||||
/// `UI-Cooldown-Indicator.m2` with the `m2anim` bone-scale dump (uniform XYZ, so one scalar per
|
||||
/// key): grow to 1.85× with the alpha rise, then a shrinking double-bounce while it fades.
|
||||
const STAR_SCALE_KEYS: [(f32, f32); 5] = [
|
||||
(0.0, 1.0),
|
||||
(0.333, 1.853),
|
||||
(0.666, 1.305),
|
||||
(0.833, 1.605),
|
||||
(1.0, 1.155),
|
||||
];
|
||||
|
||||
/// Linear sample of [`STAR_SCALE_KEYS`] at flash progress `t` (the M2 linear-track read).
|
||||
fn star_scale(t: f32) -> f32 {
|
||||
let t = t.clamp(0.0, 1.0);
|
||||
for w in STAR_SCALE_KEYS.windows(2) {
|
||||
let ((t0, v0), (t1, v1)) = (w[0], w[1]);
|
||||
if t <= t1 {
|
||||
return v0 + (v1 - v0) * ((t - t0) / (t1 - t0));
|
||||
}
|
||||
}
|
||||
STAR_SCALE_KEYS[4].1
|
||||
}
|
||||
|
||||
/// The Cooldown widget's pixels (decision 0137 phase 4): the radial dark wipe + the finish flash,
|
||||
/// rebuilt natively from the byte-read `UI-Cooldown-Indicator.m2`.
|
||||
///
|
||||
/// **The wipe.** The model draws 4 quadrant quads whose UVs rotate over a clamp-edged
|
||||
/// half-transparent texture — 2001's way of sweeping a straight edge through each quadrant; with
|
||||
/// a *flat* texture behind it (`cooldown.blp` is uniform black at α 0x99), the composed result is
|
||||
/// exactly a uniform dark pie whose bright sector grows **clockwise from 12 o'clock** as the
|
||||
/// cooldown elapses. Rebuilt as ≤4 quads: one full-dark rect per still-covered quadrant, plus
|
||||
/// the active quadrant's **wedge** — an exact convex fan (`c → ray exit → [outer corner] →
|
||||
/// quadrant-end edge midpoint`) through [`UiQuad::corners`]. Pixel-equivalent, no scissor
|
||||
/// interplay. (The wedge bypasses the scroll clip — no cooldown sits in a ScrollFrame; the
|
||||
/// full-dark rects still carry it.)
|
||||
///
|
||||
/// **The flash.** The model's sequence 1 (authored 1.000 s): the additive `star4` burst whose
|
||||
/// alpha is the byte-read texture-weight ramp — linear 0→1 over the first third, hold 1 to the
|
||||
/// half, linear 1→0 over the back half — scaled by the star bone's byte-read 5-key pulse
|
||||
/// ([`STAR_SCALE_KEYS`], the `m2anim` bone-scale dump): grow to 1.85× with the alpha rise, then
|
||||
/// a shrinking double-bounce as it fades.
|
||||
#[allow(clippy::too_many_arguments)] // one draw arm's full input set
|
||||
pub(super) fn cooldown_quads(
|
||||
rect: Rect,
|
||||
z_key: u64,
|
||||
frame_alpha: f32,
|
||||
fraction: f32,
|
||||
flash: Option<f32>,
|
||||
clip: Option<Rect>,
|
||||
assets: &mut Option<ResMut<WorldAssets>>,
|
||||
images: &mut Assets<Image>,
|
||||
out: &mut Vec<UiQuad>,
|
||||
) {
|
||||
let c = rect.center();
|
||||
let dark = [0.0, 0.0, 0.0, COOLDOWN_WIPE_ALPHA * frame_alpha];
|
||||
|
||||
if let Some(progress) = flash {
|
||||
// The finish flash: the byte-read weight ramp (keys 0→1 over 0..⅓, hold to ½, →0 at 1).
|
||||
let ramp = if progress < 1.0 / 3.0 {
|
||||
progress * 3.0
|
||||
} else if progress < 0.5 {
|
||||
1.0
|
||||
} else {
|
||||
(1.0 - progress) * 2.0
|
||||
};
|
||||
let handle = assets
|
||||
.as_mut()
|
||||
.and_then(|a| a.sprite_texture("Interface\\Cooldown\\star4", images));
|
||||
if let Some(handle) = handle {
|
||||
let s = star_scale(progress);
|
||||
let half = rect.half_size() * s;
|
||||
out.push(UiQuad {
|
||||
rect: Rect::from_center_half_size(c, half),
|
||||
z_key,
|
||||
texture: Some(handle),
|
||||
color: [1.0, 1.0, 1.0, ramp * frame_alpha],
|
||||
additive: true,
|
||||
clip,
|
||||
..default()
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// The sweep: bright sector [0, θ) clockwise from 12 o'clock; quadrants wholly past the edge
|
||||
// are full-dark rects, the active one carries the exact wedge.
|
||||
let theta = fraction.clamp(0.0, 1.0) * std::f32::consts::TAU;
|
||||
let active = ((theta / std::f32::consts::FRAC_PI_2) as usize).min(3);
|
||||
// Clockwise from 12 in y-down screen space: top-right, bottom-right, bottom-left, top-left.
|
||||
let quadrant = |k: usize| match k {
|
||||
0 => Rect::new(c.x, rect.min.y, rect.max.x, c.y),
|
||||
1 => Rect::new(c.x, c.y, rect.max.x, rect.max.y),
|
||||
2 => Rect::new(rect.min.x, c.y, c.x, rect.max.y),
|
||||
_ => Rect::new(rect.min.x, rect.min.y, c.x, c.y),
|
||||
};
|
||||
for k in (active + 1)..4 {
|
||||
out.push(UiQuad {
|
||||
rect: quadrant(k),
|
||||
z_key,
|
||||
color: dark,
|
||||
clip,
|
||||
..default()
|
||||
});
|
||||
}
|
||||
|
||||
// The active quadrant's wedge. The sweep ray from c at angle θ has direction
|
||||
// d(θ) = (sin θ, −cos θ) (y-down); per quadrant it can exit through two edges — the "first"
|
||||
// one (adjacent to the sweep's entry axis, whose crossing keeps the quadrant's outer corner
|
||||
// dark) or the "second" (past the corner). Vertices fan from c, clockwise:
|
||||
// c → E (ray exit) → [K (outer corner) if the ray exits the first edge] → M (the
|
||||
// quadrant-end axis point). A three-vertex wedge repeats M (the fan's degenerate second
|
||||
// triangle draws nothing).
|
||||
let d = Vec2::new(theta.sin(), -theta.cos());
|
||||
// Per active quadrant: (first-edge t, second-edge t, K, M). A division by ±0 yields ±inf,
|
||||
// which the min-pick handles (the boundary angles resolve to the finite edge).
|
||||
let (t_first, t_second, k_corner, m_point) = match active {
|
||||
0 => (
|
||||
(rect.min.y - c.y) / d.y,
|
||||
(rect.max.x - c.x) / d.x,
|
||||
Vec2::new(rect.max.x, rect.min.y),
|
||||
Vec2::new(rect.max.x, c.y),
|
||||
),
|
||||
1 => (
|
||||
(rect.max.x - c.x) / d.x,
|
||||
(rect.max.y - c.y) / d.y,
|
||||
Vec2::new(rect.max.x, rect.max.y),
|
||||
Vec2::new(c.x, rect.max.y),
|
||||
),
|
||||
2 => (
|
||||
(rect.max.y - c.y) / d.y,
|
||||
(rect.min.x - c.x) / d.x,
|
||||
Vec2::new(rect.min.x, rect.max.y),
|
||||
Vec2::new(rect.min.x, c.y),
|
||||
),
|
||||
_ => (
|
||||
(rect.min.x - c.x) / d.x,
|
||||
(rect.min.y - c.y) / d.y,
|
||||
Vec2::new(rect.min.x, rect.min.y),
|
||||
Vec2::new(c.x, rect.min.y),
|
||||
),
|
||||
};
|
||||
let e_point = c + d * t_first.min(t_second);
|
||||
let corners = if t_first <= t_second {
|
||||
[c, e_point, k_corner, m_point]
|
||||
} else {
|
||||
[c, e_point, m_point, m_point]
|
||||
};
|
||||
out.push(UiQuad {
|
||||
rect: quadrant(active),
|
||||
z_key,
|
||||
color: dark,
|
||||
corners: Some(corners),
|
||||
..default()
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod cooldown_quad_tests {
|
||||
use super::{cooldown_quads, star_scale, STAR_SCALE_KEYS};
|
||||
use bevy::prelude::*;
|
||||
|
||||
/// Emit the sweep for `fraction` on a unit-friendly 40×40 button at (0,0)..(40,40).
|
||||
fn sweep(fraction: f32) -> Vec<crate::ui_pass::UiQuad> {
|
||||
let mut out = Vec::new();
|
||||
cooldown_quads(
|
||||
Rect::new(0.0, 0.0, 40.0, 40.0),
|
||||
7,
|
||||
1.0,
|
||||
fraction,
|
||||
None,
|
||||
None,
|
||||
&mut None,
|
||||
&mut Assets::<Image>::default(),
|
||||
&mut out,
|
||||
);
|
||||
out
|
||||
}
|
||||
|
||||
/// Total dark coverage of the emitted quads (rect quads + the corner wedge's shoelace area).
|
||||
fn dark_area(quads: &[crate::ui_pass::UiQuad]) -> f32 {
|
||||
quads
|
||||
.iter()
|
||||
.map(|q| match q.corners {
|
||||
None => q.rect.width() * q.rect.height(),
|
||||
Some(c) => {
|
||||
// Shoelace over the fan's two triangles (0,1,2) + (0,2,3).
|
||||
let tri = |a: Vec2, b: Vec2, d: Vec2| ((b - a).perp_dot(d - a) / 2.0).abs();
|
||||
tri(c[0], c[1], c[2]) + tri(c[0], c[2], c[3])
|
||||
}
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// The square-cornered pie's exact per-phase geometry (a square pie's area is not linear in
|
||||
/// the swept angle, so the checkpoints below are the analytically known shapes: full, the
|
||||
/// 45° corner ray, the 180° half, the end sliver).
|
||||
#[test]
|
||||
fn cooldown_sweep_geometry_is_the_exact_square_pie() {
|
||||
// Fraction 0: everything dark — 3 full quadrant rects + a wedge covering the fourth.
|
||||
let q0 = sweep(0.0);
|
||||
assert_eq!(q0.len(), 4);
|
||||
assert!(
|
||||
(dark_area(&q0) - 1600.0).abs() < 1e-2,
|
||||
"fully dark at start"
|
||||
);
|
||||
|
||||
// Fraction 1/8 (45° — the ray through quadrant 0's outer corner): the bright half of
|
||||
// quadrant 0 is gone; the wedge is the triangle {center, corner, right-edge midpoint}.
|
||||
let q = sweep(0.125);
|
||||
assert_eq!(q.len(), 4);
|
||||
assert!(
|
||||
(dark_area(&q) - (1600.0 - 200.0)).abs() < 1.0,
|
||||
"45°: half of one 400px² quadrant is bright, got {}",
|
||||
dark_area(&q)
|
||||
);
|
||||
|
||||
// Fraction 0.5 (180°): the right half is bright — two full-dark quadrants + a wedge
|
||||
// that spans exactly quadrant 2 (t ties pick the corner-inclusive fan; area 400).
|
||||
let q = sweep(0.5);
|
||||
assert!((dark_area(&q) - 800.0).abs() < 1.0, "half bright at 180°");
|
||||
|
||||
// Fraction ~1: a sliver — near-zero dark area, no full quadrants left.
|
||||
let q = sweep(0.999);
|
||||
assert!(dark_area(&q) < 20.0, "almost done — a sliver remains");
|
||||
assert_eq!(q.len(), 1, "only the active quadrant's wedge remains");
|
||||
|
||||
// The wedge always fans from the button center.
|
||||
for f in [0.05, 0.3, 0.6, 0.9] {
|
||||
let q = sweep(f);
|
||||
let wedge = q
|
||||
.iter()
|
||||
.find_map(|u| u.corners)
|
||||
.expect("a wedge each phase");
|
||||
assert_eq!(wedge[0], Vec2::new(20.0, 20.0), "fan apex at the center");
|
||||
}
|
||||
}
|
||||
|
||||
/// The star pulse samples its byte-read keys exactly and lerps between them — the ⅓ peak
|
||||
/// coincides with the alpha ramp's own peak.
|
||||
#[test]
|
||||
fn star_scale_follows_the_five_key_curve() {
|
||||
for (t, v) in STAR_SCALE_KEYS {
|
||||
assert!((star_scale(t) - v).abs() < 1e-4, "key at {t}");
|
||||
}
|
||||
// Midway up the rise: halfway between 1.0 and 1.853.
|
||||
let mid = star_scale(0.333 / 2.0);
|
||||
assert!((mid - (1.0 + 1.853) / 2.0).abs() < 1e-2, "got {mid}");
|
||||
// Clamped outside the band.
|
||||
assert!((star_scale(-1.0) - 1.0).abs() < 1e-4);
|
||||
assert!((star_scale(2.0) - 1.155).abs() < 1e-4);
|
||||
}
|
||||
|
||||
/// The flash phase draws no dark pie — only the additive star (skipped here: no assets in a
|
||||
/// unit test — the pie's absence is the machine-checkable half).
|
||||
#[test]
|
||||
fn cooldown_flash_phase_emits_no_dark_pie() {
|
||||
let mut out = Vec::new();
|
||||
cooldown_quads(
|
||||
Rect::new(0.0, 0.0, 40.0, 40.0),
|
||||
7,
|
||||
1.0,
|
||||
1.2,
|
||||
Some(0.4),
|
||||
None,
|
||||
&mut None,
|
||||
&mut Assets::<Image>::default(),
|
||||
&mut out,
|
||||
);
|
||||
assert!(out.is_empty(), "no assets → no star; and never a dark quad");
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,7 @@ use crate::ui_text::UiFontAtlas;
|
||||
use benilla_assets::WorldAssets;
|
||||
|
||||
mod colorselect;
|
||||
mod cooldown;
|
||||
mod text;
|
||||
use cooldown::cooldown_quads;
|
||||
|
||||
/// `WOW_UI_COST=1` — the untraced per-frame cost meter for this system's phases (the premise
|
||||
/// instrument for the UI epoch-gate lane, 0730's warm slice): one `[ui-cost]` line per frame with
|
||||
@@ -94,7 +92,6 @@ fn report_ui_pick(eq: &benilla_ui::script::ExtractedQuad, rect: Rect, at: Vec2,
|
||||
let what = match &eq.content {
|
||||
QuadContent::Frame => "frame-slot".to_string(),
|
||||
QuadContent::Minimap { .. } => "minimap".to_string(),
|
||||
QuadContent::Cooldown { .. } => "cooldown".to_string(),
|
||||
QuadContent::Texture {
|
||||
path,
|
||||
color,
|
||||
@@ -118,7 +115,6 @@ fn content_kind(c: &QuadContent) -> &'static str {
|
||||
match c {
|
||||
QuadContent::Frame => "frame",
|
||||
QuadContent::Minimap { .. } => "minimap",
|
||||
QuadContent::Cooldown { .. } => "cooldown",
|
||||
QuadContent::ModelPane { .. } => "modelpane",
|
||||
QuadContent::Texture { .. } => "texture",
|
||||
QuadContent::ColorWheel => "colorwheel",
|
||||
@@ -354,7 +350,6 @@ fn span_bounds(spans: &[u32], i: usize) -> (usize, usize) {
|
||||
fn splice_simple(eq: &benilla_ui::script::ExtractedQuad) -> bool {
|
||||
match &eq.content {
|
||||
QuadContent::Frame
|
||||
| QuadContent::Cooldown { .. }
|
||||
| QuadContent::Backdrop { .. }
|
||||
// A model pane writes at most one quad and one idempotent tile request (decision 2008)
|
||||
// — and the map's arrow changes facing on every turn while the map is open.
|
||||
@@ -1310,14 +1305,6 @@ fn convert_entry(
|
||||
alpha: eq.alpha,
|
||||
});
|
||||
}
|
||||
// The Cooldown widget's pie wipe + finish flash (decision 0137 phase 4) — the
|
||||
// byte-pinned look of `UI-Cooldown-Indicator.m2`, rebuilt natively (see
|
||||
// [`cooldown_quads`]).
|
||||
QuadContent::Cooldown { fraction, flash } => {
|
||||
cooldown_quads(
|
||||
rect, eq.z, eq.alpha, fraction, flash, clip, assets, images, out,
|
||||
);
|
||||
}
|
||||
// A `<Model>`/`<PlayerModel>` pane's content: the off-screen body bake its window keeps,
|
||||
// sampled square edge to edge. The pane→booth join is
|
||||
// [`crate::portrait::model_pane_booth`], whose doc says why it is a table of frame names.
|
||||
@@ -1328,8 +1315,9 @@ fn convert_entry(
|
||||
// own file, which declares a bare `<PlayerModel>` and no Texture at all — so the widget
|
||||
// itself has to draw, or the character sheet's paper doll is an empty rectangle.
|
||||
//
|
||||
// A pane with no name, or one no window claims, draws nothing (`SetModel` panes included:
|
||||
// this engine holds their scene and renders no M2 into it). Nothing is stubbed white.
|
||||
// A FILE pane is a tile (2013; the map arrow among them since 2015); a UNIT pane joins
|
||||
// the booth its window keeps. A pane with no name, or one no window claims, draws
|
||||
// nothing. Nothing is stubbed white.
|
||||
QuadContent::ModelPane {
|
||||
handle,
|
||||
name,
|
||||
@@ -1341,44 +1329,6 @@ fn convert_entry(
|
||||
icon,
|
||||
} => {
|
||||
use crate::portrait::PortraitSource;
|
||||
// The world-map arrow (decision 1980): the stock `WorldMapFrame.lua` /
|
||||
// `Blizzard_BattlefieldMinimap.lua` create an anonymous `Model` child, `SetModel` it to
|
||||
// the minimap arrow, and steer it with `SetFacing`/`SetPosition` every update. benilla
|
||||
// draws that one file as the minimap's own arrow sprite — the same art the minimap
|
||||
// ring draws (`crate::minimap::blips`), turned the same way (`-facing`: the M2's yaw
|
||||
// is CCW-positive, the screen's is CW), filling the pane the engine sized to the
|
||||
// reference's 33.6 px footprint times the arrow's `SetModelScale`. No 3D pane needs
|
||||
// to exist for a two-triangle arrow.
|
||||
if model.as_deref() == Some(benilla_ui::script::ARROW_MODEL) {
|
||||
use crate::minimap::blips::{
|
||||
PLAYER_ARROW_OFFSET_PX, PLAYER_ARROW_QUAD_PX, PLAYER_ARROW_TEXTURE,
|
||||
};
|
||||
let Some(handle) = assets
|
||||
.as_mut()
|
||||
.and_then(|a| a.sprite_texture(PLAYER_ARROW_TEXTURE, images))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
// The M2's single quad sits off its origin by an authored offset that turns with
|
||||
// the facing — the minimap's arrow applies the same one at its 33.6 px basis, and
|
||||
// the pane the engine sized (`worldmap_arrow.rs`: the footprint times the
|
||||
// `SetModelScale`) scales it here. `model_scale` is already in the rect.
|
||||
let rotation = -facing;
|
||||
let (sin, cos) = rotation.sin_cos();
|
||||
let off = PLAYER_ARROW_OFFSET_PX * (rect.width() / PLAYER_ARROW_QUAD_PX);
|
||||
let off = Vec2::new(off.x * cos - off.y * sin, off.x * sin + off.y * cos);
|
||||
let rect = Rect::from_center_size(rect.center() + off, rect.size());
|
||||
out.push(UiQuad {
|
||||
rect,
|
||||
z_key: eq.z,
|
||||
texture: Some(handle),
|
||||
color: [1.0, 1.0, 1.0, eq.alpha],
|
||||
rotation,
|
||||
clip,
|
||||
..default()
|
||||
});
|
||||
return;
|
||||
}
|
||||
// A FILE pane (decision 2008): publish what the tile renderer needs — the pane's
|
||||
// device-pixel size and the render law's unit ladder off it — and draw the tile's
|
||||
// atlas cell when the renderer has one. The request is idempotent, so the memoized
|
||||
|
||||
@@ -329,11 +329,11 @@ fn dragging_the_bar_steps_the_offset_by_rows_and_repaints() {
|
||||
/// on `FauxScrollFrameTemplate` whose `<OnVerticalScroll>` calls `FauxScrollFrame_OnVerticalScroll`,
|
||||
/// driven by a drag of the shared bar.
|
||||
///
|
||||
/// The `SetScrollChild` line is the gap, made visible: the reference's template declares that child
|
||||
/// as `<ScrollChild><Frame name="$parentScrollChildFrame">`, our XML loader has no `<ScrollChild>`
|
||||
/// element, and `SetVerticalScroll` clamps into `[0, GetVerticalScrollRange()]` — which is computed
|
||||
/// from that child. Without it the range is 0, the handler fires with `arg1 = 0` and an addon's list
|
||||
/// never leaves the top. With it, the whole reference path runs.
|
||||
/// The `SetScrollChild` line predates 1205 (the loader's `<ScrollChild>`) and stays as the
|
||||
/// addon-shaped way of seating a child by hand; the range it asserts is that child's overflow
|
||||
/// (1338). The offset itself never depended on the range — the engine stores what the bar hands it
|
||||
/// (decision 2017) — so what this drives is bar value → `SetVerticalScroll` → `<OnVerticalScroll>`
|
||||
/// → `FauxScrollFrame_OnVerticalScroll`'s `floor(v / step + 0.5)`, the reference's path end to end.
|
||||
#[test]
|
||||
fn the_reference_on_vertical_scroll_path_runs_once_a_scroll_child_exists() {
|
||||
let mut s = harness();
|
||||
|
||||
@@ -101,7 +101,6 @@ const FRAME_TAGS: &[&str] = &[
|
||||
"Minimap",
|
||||
"MovieFrame",
|
||||
"WorldFrame",
|
||||
"Cooldown",
|
||||
];
|
||||
|
||||
/// The five `<Scripts>` handler names that auto-enable the MOUSE kind, and only those five
|
||||
|
||||
@@ -672,3 +672,70 @@ fn selecting_a_row_reads_back_in_the_same_tick() {
|
||||
.take_social_requests()
|
||||
.contains(&SocialRequest::SelectFriend(2)));
|
||||
}
|
||||
|
||||
/// **B363 — the who list reaches its last rows.** Liho's `/who` found 49, showed 17, and the knob
|
||||
/// travelled while the rows stayed. On the stock window the mechanism is a one-row loss:
|
||||
/// `WhoListScrollFrame` is 287 tall (stock `FriendsFrame.xml` l.1661) against seventeen rows of
|
||||
/// sixteen, so the child's overflow past the frame, `n × 16 − 287`, sits fifteen pixels under the
|
||||
/// bar's `(n − 17) × 16`, and an engine that clamped `SetVerticalScroll` into that overflow
|
||||
/// stopped the row offset at `n − 18`. The reference stores the bar's value as given (decision
|
||||
/// 2017). Drives the bar to its end and reads the seventeenth row: the forty-ninth name.
|
||||
#[test]
|
||||
fn the_who_list_reaches_its_last_row_at_the_bars_end() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = setup();
|
||||
s.run("ShowWhoPanel()").unwrap();
|
||||
let who: Vec<WhoInfo> = (1..=49)
|
||||
.map(|i| WhoInfo {
|
||||
name: format!("Who{i:02}"),
|
||||
guild: String::new(),
|
||||
level: 60,
|
||||
race: "Human".to_string(),
|
||||
class: "Warrior".to_string(),
|
||||
zone: "Elwynn Forest".to_string(),
|
||||
})
|
||||
.collect();
|
||||
push(
|
||||
&mut s,
|
||||
SocialState {
|
||||
who,
|
||||
who_total: 49,
|
||||
..Default::default()
|
||||
},
|
||||
"WHO_LIST_UPDATE",
|
||||
);
|
||||
s.resolve();
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
|
||||
// The reference's own numbers, and the control: the overflow is shorter than the bar.
|
||||
let (_, bar_max) = s
|
||||
.eval::<(f64, f64)>("return WhoListScrollFrameScrollBar:GetMinMaxValues()")
|
||||
.unwrap();
|
||||
assert_eq!(bar_max, 512.0, "(49 − 17) × 16");
|
||||
let overflow = s
|
||||
.eval::<f64>("return WhoListScrollFrame:GetVerticalScrollRange()")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
overflow, 497.0,
|
||||
"49 × 16 − 287: the frame is taller than its rows"
|
||||
);
|
||||
|
||||
s.run("WhoListScrollFrameScrollBar:SetValue(512)").unwrap();
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return FauxScrollFrame_GetOffset(WhoListScrollFrame)")
|
||||
.unwrap(),
|
||||
32
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return WhoFrameButton1Name:GetText()")
|
||||
.unwrap(),
|
||||
"Who33"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return WhoFrameButton17Name:GetText()")
|
||||
.unwrap(),
|
||||
"Who49",
|
||||
"the last hit is on the last row"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -582,7 +582,7 @@ fn the_world_map_cannot_open_behind_the_menu_and_gives_its_slot_back() {
|
||||
r"Interface\FrameXML\UIPanelTemplates.xml",
|
||||
// The stock map's OnShow/OnHide call `UpdateMicroButtons` unguarded (1980), which the
|
||||
// micro menu defines over the action bar it sits in — the spellbook kit's cluster.
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
@@ -643,7 +643,7 @@ fn nothing_opens_behind_the_world_map_and_escape_closes_it_first() {
|
||||
r"Interface\FrameXML\UIPanelTemplates.xml",
|
||||
// The action-bar cluster the micro menu sits in — the stock map's OnShow/OnHide call
|
||||
// `UpdateMicroButtons` unguarded (1980).
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
@@ -704,7 +704,7 @@ fn the_bag_row_greys_under_the_menu_without_any_of_it_disappearing() {
|
||||
// BagFrame.xml precisely because most harnesses load no bar).
|
||||
let mut s = bag_harness_with(
|
||||
&[
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
|
||||
@@ -83,7 +83,7 @@ fn setup() -> UiScript {
|
||||
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml"); // PASS/NEED/GREED + item hovers
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.lua");
|
||||
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.xml");
|
||||
|
||||
@@ -39,7 +39,7 @@ fn setup() -> UiScript {
|
||||
// The stock file's `HelpFrame_OnShow` calls `UpdateMicroButtons()` before
|
||||
// `GetGMStatus()`, so without the micro row the OnShow raises and the status ask never
|
||||
// happens. Ours never called it. The row needs the bar's button kit under it.
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
|
||||
@@ -32,7 +32,7 @@ fn harness() -> UiScript {
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
for file in [
|
||||
"Interface\\FrameXML\\Fonts.xml",
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
|
||||
@@ -23,7 +23,7 @@ fn load_action_bar(s: &UiScript) {
|
||||
r"Interface\FrameXML\MoneyFrame.lua",
|
||||
r"Interface\FrameXML\MoneyFrame.xml",
|
||||
"Interface\\FrameXML\\GameTooltip.xml",
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
@@ -276,7 +276,7 @@ fn shipped_stance_bar_drives_end_to_end() {
|
||||
// bottom multibar is UP to raise the flag. Since 1500 that is a player option rather than a
|
||||
// given, so this test raises it; `the_stance_bar_sits_where_the_pass_puts_it` below owns the
|
||||
// other state.
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
||||
@@ -465,7 +465,7 @@ fn multibar_hover_renders_the_buttons_own_action() {
|
||||
r"Interface\FrameXML\MoneyFrame.lua",
|
||||
r"Interface\FrameXML\MoneyFrame.xml",
|
||||
"Interface\\FrameXML\\GameTooltip.xml",
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
@@ -579,7 +579,7 @@ fn multibar_hover_renders_the_buttons_own_action() {
|
||||
fn the_vertical_multibars_exist_hidden_on_the_reference_pages() {
|
||||
let s = UiScript::new().unwrap();
|
||||
load_action_bar(&s);
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
||||
@@ -641,7 +641,7 @@ fn every_extra_bar_stays_down_until_its_own_toggle_is_set() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_action_bar(&s);
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
||||
@@ -728,7 +728,7 @@ fn raising_a_bottom_bar_moves_the_managed_bottom_stack() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_action_bar(&s);
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
||||
@@ -792,7 +792,7 @@ fn viewable_action_bar_pages_follow_the_bar_toggles() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_action_bar(&s);
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
||||
@@ -858,7 +858,7 @@ fn the_grid_option_holds_the_extra_bars_empty_wells_open() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_action_bar(&s);
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
||||
@@ -961,7 +961,7 @@ fn a_held_payload_ghosts_the_empty_wells_it_opens() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_action_bar(&s);
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
||||
@@ -1104,7 +1104,7 @@ fn a_bar_toggle_sends_the_byte_its_globals_pack_to() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_action_bar(&s);
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
||||
@@ -1191,7 +1191,7 @@ fn the_shipped_setter_passes_exactly_four_arguments() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_action_bar(&s);
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
||||
@@ -1243,7 +1243,7 @@ fn the_stance_bar_sits_where_the_pass_puts_it() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_action_bar(&s);
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
||||
@@ -1257,7 +1257,7 @@ fn the_stance_bar_sits_where_the_pass_puts_it() {
|
||||
load_xml(&s, "KeyBindingsPage.xml");
|
||||
load_xml(&s, "OptionsFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
s.set_shapeshift_forms(vec![ShapeshiftFormView {
|
||||
spell_id: 2457,
|
||||
@@ -1315,7 +1315,7 @@ fn the_stance_shelf_follows_the_bottom_left_bar() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_action_bar(&s);
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
||||
@@ -1329,7 +1329,7 @@ fn the_stance_shelf_follows_the_bottom_left_bar() {
|
||||
load_xml(&s, "KeyBindingsPage.xml");
|
||||
load_xml(&s, "OptionsFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
|
||||
let form = |id: u32| ShapeshiftFormView {
|
||||
@@ -1436,7 +1436,7 @@ fn the_stance_shelf_is_as_long_as_the_form_count() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_action_bar(&s);
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
||||
@@ -1450,7 +1450,7 @@ fn the_stance_shelf_is_as_long_as_the_form_count() {
|
||||
load_xml(&s, "KeyBindingsPage.xml");
|
||||
load_xml(&s, "OptionsFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
show_bars(&s, &[]); // unraised: the shelf art is the state under test
|
||||
|
||||
@@ -1553,7 +1553,7 @@ fn an_extra_bars_empty_well_keeps_its_bound_hotkey_label() {
|
||||
// the player binds it, as the reporter had).
|
||||
s.register_bindings(&crate::bindings::registry_commands());
|
||||
load_action_bar(&s);
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
||||
|
||||
@@ -775,7 +775,7 @@ fn interface_harness() -> UiScript {
|
||||
// VARIABLES_LOADED fires the slider exists. A test that fires the event has to have
|
||||
// loaded it too.
|
||||
"Interface\\FrameXML\\ColorPickerFrame.xml",
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"Interface\\FrameXML\\MainMenuBar.xml",
|
||||
"Interface\\FrameXML\\ActionBarFrame.xml",
|
||||
@@ -850,7 +850,7 @@ fn actionbars_harness() -> UiScript {
|
||||
&[
|
||||
"Interface\\FrameXML\\Fonts.xml",
|
||||
r"Interface\FrameXML\UIParent.xml",
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
|
||||
@@ -850,7 +850,7 @@ fn gossip_bank_option_hands_the_left_slot_to_the_bank() {
|
||||
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
|
||||
// UIPanelScrollFrameTemplate lives here, and the gossip scroll frame inherits it. NOT
|
||||
|
||||
@@ -15,7 +15,7 @@ fn harness(extra: &[&str]) -> UiScript {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.lua");
|
||||
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.xml");
|
||||
|
||||
@@ -19,7 +19,7 @@ pub(super) fn load_pet_bar(s: &UiScript) {
|
||||
for file in [
|
||||
"Interface\\FrameXML\\Fonts.xml",
|
||||
r"Interface\FrameXML\UIParent.xml",
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
|
||||
@@ -1225,7 +1225,7 @@ fn selection_change_resets_detail_scroll_but_a_quest_log_update_refresh_does_not
|
||||
s.resolve();
|
||||
answer_measures(&mut s);
|
||||
s.resolve(); // the measured span reaches the scroll range, and the bar, on this pass
|
||||
s.resolve(); // GetVerticalScrollRange (SetVerticalScroll's clamp) reads resolved rects.
|
||||
s.resolve(); // GetVerticalScrollRange (the bar's [min, max]) reads resolved rects.
|
||||
|
||||
// Scroll THROUGH the bar, as the wheel and the thumb do (ScrollFrameTemplate_OnMouseWheel sets
|
||||
// the bar; its OnValueChanged moves the frame): a reselect's `ScrollBar:SetValue(0)` is what
|
||||
|
||||
@@ -760,7 +760,7 @@ mod tests {
|
||||
"ScrollFrame",
|
||||
"Slider", "ColorSelect", "MessageFrame", "ScrollingMessageFrame",
|
||||
"SimpleHTML", "Model", "PlayerModel", "DressUpModel", "TabardModel",
|
||||
"Minimap", "MovieFrame", "Cooldown",
|
||||
"Minimap", "MovieFrame",
|
||||
}}
|
||||
for i = 1, table.getn(types) do
|
||||
local ok, w = pcall(function()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! The shipped **Skills tab** (`assets/ui/SkillFrame.xml`) driven end-to-end, engine-only (no
|
||||
//! Bevy) — the per-window test module the spellbook/trainer/bank files already establish, split
|
||||
//! The stock **Skills tab** (`Interface\FrameXML\SkillFrame.xml`, off the player's chain since
|
||||
//! 1956) driven end-to-end, engine-only (no Bevy) — the per-window test module the spellbook/trainer/bank files already establish, split
|
||||
//! out of `character_tests.rs` (which owns the paperdoll + the tab round-trip) so the skills-pane
|
||||
//! paint law has a home of its own.
|
||||
//!
|
||||
@@ -12,10 +12,9 @@
|
||||
//! hunter's `Beast Mastery` on vmangos arrives as `300/300` and must still read gray and
|
||||
//! numberless, exactly as it does in the real client.
|
||||
//!
|
||||
//! **The window around the page is the reference's own since 1751** — `CharacterFrame.xml` and
|
||||
//! `PaperDollFrame.xml` off the player's chain — so every test that opens the page loads
|
||||
//! [`super::test_ui::CHARACTER_UI`] and opens with `wow_data_or_skip!()`. The page itself
|
||||
//! (`assets/ui/SkillFrame.xml`) is still ours.
|
||||
//! **The whole window is the reference's own** — `CharacterFrame.xml` and `PaperDollFrame.xml`
|
||||
//! since 1751, the page itself since 1956 — so every test that opens the page loads
|
||||
//! [`super::test_ui::CHARACTER_UI`] and opens with `wow_data_or_skip!()`.
|
||||
|
||||
use benilla_ui::script::{QuadContent, SkillEntry, SkillsState, UiScript, UnitState};
|
||||
|
||||
@@ -506,3 +505,137 @@ fn the_expand_tab_fits_its_label_at_load() {
|
||||
);
|
||||
assert!(s.errors().is_empty(), "errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// **B370 — the list reaches its last rows.** MarcusAga's Skills tab stopped three rows short of
|
||||
/// the end: the knob mid-track, Maces the last row shown. The reference's own
|
||||
/// `FauxScrollFrame_Update` sizes the bar to `(n − 12) × 15` and the scroll child to `n × 15`, and
|
||||
/// `SkillListScrollFrame` is 220 tall (stock `SkillFrame.xml` l.468) against twelve rows of
|
||||
/// fifteen — so the child's overflow past the frame, `n × 15 − 220`, is forty pixels short of
|
||||
/// where the bar goes. An engine that clamped `SetVerticalScroll` into that overflow stopped the
|
||||
/// row offset at `n − 15`; the reference stores the bar's value as given (decision 2017).
|
||||
///
|
||||
/// Drives the bar to its end and reads the twelfth row: the block's last line. The control is the
|
||||
/// pre-fix mechanism itself — the overflow really is shorter than the bar's range.
|
||||
#[test]
|
||||
fn the_list_reaches_its_last_row_at_the_bars_end() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
for f in super::test_ui::CHARACTER_UI {
|
||||
super::test_ui::load_ui_strict(&s, f);
|
||||
}
|
||||
s.set_unit(
|
||||
"player",
|
||||
Some(UnitState {
|
||||
exists: true,
|
||||
level: 60,
|
||||
race: Some("Human".into()),
|
||||
race_file: Some("Human".into()),
|
||||
class: Some("Warrior".into()),
|
||||
class_file: Some("WARRIOR".into()),
|
||||
..UnitState::default()
|
||||
}),
|
||||
);
|
||||
// A warrior's kind of block — twenty-two lines under three headers, twenty-five rows: more than
|
||||
// twelve by more than the frame's slack, so the tail is only reachable past the overflow.
|
||||
let mut entries = Vec::new();
|
||||
for (i, name) in [
|
||||
"Axes",
|
||||
"Bows",
|
||||
"Crossbows",
|
||||
"Daggers",
|
||||
"Defense",
|
||||
"Guns",
|
||||
"Maces",
|
||||
"Polearms",
|
||||
"Staves",
|
||||
"Swords",
|
||||
"Thrown",
|
||||
"Two-Handed Axes",
|
||||
"Two-Handed Maces",
|
||||
"Two-Handed Swords",
|
||||
"Unarmed",
|
||||
]
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
entries.push(skill(
|
||||
100 + i as u32,
|
||||
name,
|
||||
300,
|
||||
300,
|
||||
false,
|
||||
(6, "Weapon Skills", 5),
|
||||
));
|
||||
}
|
||||
for (i, name) in ["Cloth", "Leather", "Mail", "Plate", "Shield"]
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
entries.push(skill(
|
||||
400 + i as u32,
|
||||
name,
|
||||
1,
|
||||
1,
|
||||
false,
|
||||
(8, "Armor Proficiencies", 6),
|
||||
));
|
||||
}
|
||||
for (i, name) in ["Language: Common", "Language: Dwarven"].iter().enumerate() {
|
||||
entries.push(skill(
|
||||
500 + i as u32,
|
||||
name,
|
||||
300,
|
||||
300,
|
||||
false,
|
||||
(10, "Languages", 8),
|
||||
));
|
||||
}
|
||||
s.set_skills(SkillsState { entries });
|
||||
s.run(r#"ToggleCharacter("SkillFrame")"#).unwrap();
|
||||
s.resolve();
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
|
||||
let n = s.eval::<i64>("return GetNumSkillLines()").unwrap();
|
||||
assert_eq!(n, 25, "22 lines under 3 headers");
|
||||
let (last_name, last_is_header) = s
|
||||
.eval::<(String, Option<i64>)>(&format!("local n, h = GetSkillLineInfo({n}) return n, h"))
|
||||
.unwrap();
|
||||
assert!(
|
||||
last_is_header.is_none(),
|
||||
"the tail row is a line, read off SkillRankFrame12"
|
||||
);
|
||||
|
||||
// The reference's own numbers: the bar runs to (n − 12) × 15, the child's overflow past the
|
||||
// 220-tall frame is 40 px less — the control that the reported mechanism is the real one.
|
||||
let (_, bar_max) = s
|
||||
.eval::<(f64, f64)>("return SkillListScrollFrameScrollBar:GetMinMaxValues()")
|
||||
.unwrap();
|
||||
assert_eq!(bar_max, f64::from((n as i32 - 12) * 15));
|
||||
let overflow = s
|
||||
.eval::<f64>("return SkillListScrollFrame:GetVerticalScrollRange()")
|
||||
.unwrap();
|
||||
assert_eq!(overflow, f64::from(n as i32 * 15 - 220));
|
||||
assert!(
|
||||
overflow < bar_max,
|
||||
"the frame is taller than its twelve rows"
|
||||
);
|
||||
|
||||
// The knob dragged to the end: bar → SetVerticalScroll → <OnVerticalScroll> →
|
||||
// FauxScrollFrame_OnVerticalScroll → SkillFrame_UpdateSkills.
|
||||
s.run("SkillListScrollFrameScrollBar:SetValue(select(2, SkillListScrollFrameScrollBar:GetMinMaxValues()))")
|
||||
.unwrap();
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return FauxScrollFrame_GetOffset(SkillListScrollFrame)")
|
||||
.unwrap(),
|
||||
n - 12,
|
||||
"the row offset reaches the bar's end, not the overflow's"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return SkillRankFrame12SkillName:GetText()")
|
||||
.unwrap(),
|
||||
last_name,
|
||||
"the twelfth row is the block's last line"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ pub(super) fn spellbook_ui(w: f32, h: f32) -> UiScript {
|
||||
for f in [
|
||||
"Interface\\FrameXML\\Fonts.xml",
|
||||
r"Interface\FrameXML\UIParent.xml",
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
@@ -268,23 +268,19 @@ fn shipped_spellbook_shows_the_cooldown_pie() {
|
||||
b.slots[0].cooldown = Some((6_000, 10_000, true));
|
||||
s.set_spellbook(b);
|
||||
s.fire_event("SPELL_UPDATE_COOLDOWN", vec![]);
|
||||
// The stock machine (decision 2019): sequence 0 armed by `CooldownFrame_SetTimer`, scrubbed
|
||||
// by the next paint's `OnUpdateModel`.
|
||||
super::test_ui::cooldown_facts(&mut s);
|
||||
s.tick(0.0);
|
||||
s.resolve();
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
let sweep = s.extract().into_iter().find_map(|q| {
|
||||
if s.quad_owner_name(q.target).as_deref() != Some("SpellButton1Cooldown") {
|
||||
return None;
|
||||
}
|
||||
match q.content {
|
||||
QuadContent::Cooldown { fraction, flash } => Some((fraction, flash)),
|
||||
_ => None,
|
||||
}
|
||||
});
|
||||
let (fraction, flash) = sweep.expect("SpellButton1's Cooldown widget is showing");
|
||||
assert!(
|
||||
(fraction - 0.4).abs() < 1e-3,
|
||||
"4 s elapsed of 10 ⇒ the sweep sits at 40%, got {fraction}"
|
||||
let play = super::test_ui::cooldown_play(&s, "SpellButton1Cooldown")
|
||||
.expect("SpellButton1's cooldown pane is showing");
|
||||
assert_eq!(
|
||||
play,
|
||||
(0, 400),
|
||||
"4 s elapsed of 10 ⇒ the sweep sits at 40 %: sequence 0 at 400 ms"
|
||||
);
|
||||
assert_eq!(flash, None);
|
||||
|
||||
// An on-hold triple: no sweep (CooldownFrame_SetTimer's enable gate), the icon dims to 40%.
|
||||
let mut b = book();
|
||||
@@ -706,7 +702,7 @@ fn the_macro_editor_takes_a_shift_click_and_only_a_shift_click() {
|
||||
"Interface\\FrameXML\\StaticPopup.xml",
|
||||
// `ShowMacroFrame` lives here since 1848.
|
||||
"Interface\\FrameXML\\GameTooltip.xml",
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\Cooldown.xml",
|
||||
// **ScrollTemplates BEFORE UIPanelTemplates, which is the manifest's own order.** Ours
|
||||
// still carries dead `FauxScrollFrame_*` copies that the chain overrides by loading after
|
||||
// (1846's step 3, deliberately not done); load them the other way round and OUR copies win
|
||||
|
||||
@@ -24,7 +24,7 @@ fn harness() -> UiScript {
|
||||
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml"); // TOOLTIP_DEFAULT_* (the dropdown kit's MenuBackdrop)
|
||||
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml"); // the unit popups' kit (TargetFrameDropDown's template)
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.lua");
|
||||
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.xml");
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
//! catch a mistake in a file somebody just edited, and `content::read`'s dev-build probe already
|
||||
//! prefers disk for the same reason.
|
||||
|
||||
use benilla_ui::script::UiScript;
|
||||
use benilla_ui::script::{QuadContent, UiScript};
|
||||
|
||||
/// Load one interface file into `s`, panicking on any loader error, and return how many frames it
|
||||
/// materialized (`0` for a `.lua` entry, which materializes none).
|
||||
@@ -31,6 +31,70 @@ pub(crate) fn load_ui(s: &UiScript, entry: &str) -> usize {
|
||||
load_entry(s, entry, false, false)
|
||||
}
|
||||
|
||||
/// The cooldown indicator's file: `Interface\Cooldown\UI-Cooldown-Indicator.mdx`, as the
|
||||
/// stock `CooldownFrameTemplate` names it.
|
||||
pub(super) const COOLDOWN_MODEL: &str = r"Interface\Cooldown\UI-Cooldown-Indicator.mdx";
|
||||
|
||||
/// The cooldown indicator's file facts (`benilla-extract m2seq`): sequence 0 = id 0, 1000 ms,
|
||||
/// clamp — the sweep `CooldownFrame_OnUpdateModel` scrubs; sequence 1 = id 1, 1000 ms, clamp —
|
||||
/// the finish flash, whose completion hides the frame. Handed to the engine the way the app does
|
||||
/// once the asset lands (decisions 2007/2019); every pane holding the file arms its Stand.
|
||||
pub(super) fn cooldown_facts(s: &mut UiScript) {
|
||||
use benilla_ui::widget::{ModelFileFacts, SequenceFacts};
|
||||
let seq = |anim_id, duration_ms| SequenceFacts {
|
||||
anim_id,
|
||||
duration_ms,
|
||||
looping: false,
|
||||
};
|
||||
s.set_model_facts(
|
||||
COOLDOWN_MODEL,
|
||||
ModelFileFacts {
|
||||
sequences: vec![seq(0, 1000), seq(1, 1000)],
|
||||
bbox: ([0.0; 3], [0.0; 3]),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// The play head `(anim_id, cursor_ms)` of the SHOWN cooldown pane `owner` names (`None` while
|
||||
/// it is hidden, or has nothing armed) — what the tile renderer samples the file at. Read the way
|
||||
/// the renderer reads it: the pane's quad in the extract joined to the engine's paint list.
|
||||
pub(super) fn cooldown_play(s: &UiScript, owner: &str) -> Option<(u16, u32)> {
|
||||
let heads = s.visible_model_panes();
|
||||
s.extract().into_iter().find_map(|q| match &q.content {
|
||||
QuadContent::ModelPane {
|
||||
handle,
|
||||
model: Some(m),
|
||||
..
|
||||
} if m.eq_ignore_ascii_case(COOLDOWN_MODEL)
|
||||
&& s.quad_owner_name(q.target).as_deref() == Some(owner) =>
|
||||
{
|
||||
heads
|
||||
.iter()
|
||||
.find(|p| p.handle == *handle)
|
||||
.and_then(|p| p.play.map(|ph| (ph.anim_id, ph.cursor_ms)))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// [`cooldown_play`] for whichever cooldown pane is shown — the bag windows number their slots
|
||||
/// from the far end (`ContainerFrame_GenerateFrame`: `Item{j}` carries id `size − j + 1`), so a
|
||||
/// test that seats one item asks for "the" sweep rather than a name.
|
||||
pub(super) fn cooldown_play_any(s: &UiScript) -> Option<(u16, u32)> {
|
||||
let heads = s.visible_model_panes();
|
||||
s.extract().into_iter().find_map(|q| match &q.content {
|
||||
QuadContent::ModelPane {
|
||||
handle,
|
||||
model: Some(m),
|
||||
..
|
||||
} if m.eq_ignore_ascii_case(COOLDOWN_MODEL) => heads
|
||||
.iter()
|
||||
.find(|p| p.handle == *handle)
|
||||
.and_then(|p| p.play.map(|ph| (ph.anim_id, ph.cursor_ms))),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// [`load_ui`], and **a missing template is a failure too**.
|
||||
///
|
||||
/// A frame that inherits a template nothing declares is a loader *warning*, not an error: the frame
|
||||
@@ -404,7 +468,7 @@ pub(super) const CHARACTER_UI: &[&str] = &[
|
||||
r"Interface\FrameXML\MoneyFrame.xml",
|
||||
r"Interface\FrameXML\UIParent.xml", // Model_OnLoad/_Rotate*/_OnUpdate — the model panes' turntable
|
||||
"Interface\\FrameXML\\GameTooltip.xml",
|
||||
"Cooldown.xml", // CooldownFrameTemplate + CooldownFrame_SetTimer, per equipment slot
|
||||
"Interface\\FrameXML\\Cooldown.xml", // CooldownFrameTemplate + CooldownFrame_SetTimer, per equipment slot
|
||||
r"Interface\FrameXML\UIPanelTemplates.lua",
|
||||
r"Interface\FrameXML\UIPanelTemplates.xml",
|
||||
"Interface\\FrameXML\\StaticPopup.xml",
|
||||
@@ -470,7 +534,7 @@ pub(super) const SOCIAL_UI: &[&str] = &[
|
||||
"Interface\\FrameXML\\LocaleProperties.lua",
|
||||
"Interface\\FrameXML\\BasicControls.xml",
|
||||
r"Interface\FrameXML\UIParent.xml",
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
@@ -540,7 +604,7 @@ pub(super) const BAG_UI: &[&str] = &[
|
||||
r"Interface\FrameXML\MoneyFrame.xml",
|
||||
"Interface\\FrameXML\\LocaleProperties.lua",
|
||||
"Interface\\FrameXML\\GameTooltip.xml",
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\Cooldown.xml",
|
||||
// The bag BAR declares `parent="MainMenuBarArtFrame"`, resolved at LOAD — so without this the
|
||||
// six buttons fall back to UIParent and sit at a level no production run ever puts them at.
|
||||
// It also carries `MainMenuBar_UpdateKeyRing`, which is what puts the keyring on the bar.
|
||||
@@ -588,9 +652,8 @@ pub(super) const BAG_UI: &[&str] = &[
|
||||
"Interface\\FrameXML\\UIPanelTemplates.lua",
|
||||
"Interface\\FrameXML\\UIPanelTemplates.xml",
|
||||
"Interface\\FrameXML\\FloatingChatFrame.xml",
|
||||
// Our adapters over the reference's container files — the keyring tooltip wrapper, the three
|
||||
// bag verbs 0561 shadows (`OpenBackpack`/`CloseBackpack`/`CloseAllBags`), and the item-push
|
||||
// card the reference draws with a `<Model>` this engine does not render (0887). It has to be
|
||||
// Our adapters over the reference's container files — the keyring tooltip wrapper and the
|
||||
// three bag verbs 0561 shadows (`OpenBackpack`/`CloseBackpack`/`CloseAllBags`). It has to be
|
||||
// AFTER `ContainerFrame.xml` and after the bar, which is why it is here and not up with
|
||||
// UiPanels.xml.
|
||||
"ContainerFrameAdapters.xml",
|
||||
|
||||
@@ -241,7 +241,7 @@ fn your_own_portrait_explains_the_menu_instead_of_showing_your_health() {
|
||||
#[test]
|
||||
fn action_button_hover_takes_the_default_corner() {
|
||||
let mut s = harness(&[
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
@@ -282,7 +282,7 @@ fn action_button_hover_takes_the_default_corner() {
|
||||
#[test]
|
||||
fn ubertooltips_off_seats_action_bar_plates_beside_the_button() {
|
||||
let mut s = harness(&[
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
@@ -362,7 +362,7 @@ fn ubertooltips_off_seats_action_bar_plates_beside_the_button() {
|
||||
#[test]
|
||||
fn ubertooltips_off_seats_stance_plates_beside_the_button() {
|
||||
let mut s = harness(&[
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
@@ -415,7 +415,7 @@ fn ubertooltips_off_seats_stance_plates_beside_the_button() {
|
||||
#[test]
|
||||
fn buff_hover_hangs_below_left_of_the_button() {
|
||||
let mut s = harness(&[
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
|
||||
@@ -68,7 +68,7 @@ fn load_tot() -> UiScript {
|
||||
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UnitPopup.xml");
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.lua");
|
||||
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.xml");
|
||||
|
||||
@@ -49,7 +49,7 @@ fn trainer_script_base() -> UiScript {
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Interface\\FrameXML\\BasicControls.xml",
|
||||
r"Interface\FrameXML\UIParent.xml",
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
|
||||
@@ -506,7 +506,7 @@ fn load_pet_menu_frames(s: &UiScript) {
|
||||
"Interface\\FrameXML\\PartyFrame.xml",
|
||||
"Interface\\FrameXML\\TargetFrame.xml",
|
||||
"Interface\\FrameXML\\PetFrame.xml",
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"Interface\\FrameXML\\MainMenuBar.xml",
|
||||
"Interface\\FrameXML\\ActionBarFrame.xml",
|
||||
|
||||
@@ -51,6 +51,24 @@ fn harness() -> UiScript {
|
||||
/// The stock per-frame update, driven the way its `OnUpdate` script runs it: `this` is the map
|
||||
/// button and the elapsed time its one argument. Everything on the sheet — the arrow, the party
|
||||
/// and raid blips, the battleground teammates and flags, the corpse — is seated here.
|
||||
/// The map arrow's file facts (`MinimapArrow.m2`: one looping 3.333 s Stand keying no bone; the
|
||||
/// header box `x ∈ [−0.0127, 0.0135]`, `y ∈ [−0.0118, 0.0145]` — render law §2), handed to the
|
||||
/// engine the way the app does once the asset lands (2007/2015).
|
||||
fn arrow_facts(s: &mut UiScript) {
|
||||
use benilla_ui::widget::{ModelFileFacts, SequenceFacts};
|
||||
s.set_model_facts(
|
||||
ARROW_MODEL,
|
||||
ModelFileFacts {
|
||||
sequences: vec![SequenceFacts {
|
||||
anim_id: 0,
|
||||
duration_ms: 3333,
|
||||
looping: true,
|
||||
}],
|
||||
bbox: ([-0.0127, -0.0118, 0.0], [0.0135, 0.0145, 0.0]),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn update(s: &mut UiScript) {
|
||||
s.run("this = WorldMapButton WorldMapButton_OnUpdate(0.1) this = nil")
|
||||
.unwrap();
|
||||
@@ -336,6 +354,7 @@ fn the_player_arrow_is_the_stock_model_pane_seated_and_turned_by_the_update() {
|
||||
);
|
||||
let failures = super::load_default_ui(&s);
|
||||
assert!(failures.is_empty(), "manifest load errors: {failures:#?}");
|
||||
arrow_facts(&mut s);
|
||||
s.resolve();
|
||||
s.run("ShowUIPanel(WorldMapFrame)").unwrap();
|
||||
s.resolve();
|
||||
@@ -354,9 +373,20 @@ fn the_player_arrow_is_the_stock_model_pane_seated_and_turned_by_the_update() {
|
||||
pane.content
|
||||
);
|
||||
let rect = pane.rect.expect("…with a resolved rect");
|
||||
// The implicit rect (2015): the file's box in layout units — at 16:9 a layout unit is
|
||||
// 768·√((16/9)²+1) = 1566.4 FrameXML units, so 0.0262 × 0.0263 reads 41.0 × 41.2.
|
||||
assert!(
|
||||
rect.right > rect.left && rect.top > rect.bottom,
|
||||
"sized: {rect:?}"
|
||||
(rect.right - rect.left - 0.0262 * 1566.4).abs() < 0.2
|
||||
&& (rect.top - rect.bottom - 0.0263 * 1566.4).abs() < 0.2,
|
||||
"sized by the file's box: {rect:?}"
|
||||
);
|
||||
// …and the model is re-centred on it every update (`0x4a7b20`'s ½·GetWidth, ½·GetHeight in
|
||||
// layout units = half the box).
|
||||
assert!(
|
||||
matches!(&pane.content, QuadContent::ModelPane { position, .. }
|
||||
if (position.0 - 0.0131).abs() < 1e-4 && (position.1 - 0.01315).abs() < 1e-4),
|
||||
"{:?}",
|
||||
pane.content
|
||||
);
|
||||
// The seat, read back in Lua: the arrow is anonymous, so it is found among the map's
|
||||
// children by kind and compared against the mouseover button the update seats at the same
|
||||
@@ -615,6 +645,7 @@ fn the_player_arrow_draws_over_the_zones_explored_overlays() {
|
||||
);
|
||||
assert!(super::load_default_ui(&s).is_empty());
|
||||
s.resolve();
|
||||
arrow_facts(&mut s);
|
||||
s.run("ShowUIPanel(WorldMapFrame)").unwrap();
|
||||
s.resolve();
|
||||
s.set_world_map_feed(None, Some((0.5, 0.5)), 0.0, None, Vec::new(), Vec::new());
|
||||
|
||||
Reference in New Issue
Block a user