dialogs come to the front, and the red line names what it needs
- dialogs carry the reference's toplevel bit, so the stack-split window opens above Bagnon instead of behind it - the failure line fills its blank — "Requires Blacksmith Hammer", not "Requires" — and Opening casts stop printing a label the spell itself marks "no text" - the pet bar's Attack/Follow/Stay tooltips name their keybinding, and typed text reaches the focused frame through the client's own delivery walk - AtlasLoot parses again: our Lua keeps 5.0's compatibility semicolon Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1006,10 +1006,9 @@ fn shift_click_on_a_stack_opens_the_split_frame() {
|
||||
/// B180 — the split dialog's parchment plate fills the whole 172×96 frame. The reference authors
|
||||
/// the plate 256×32 with NO anchors, a vestigial size the real client never renders (the TexCoords
|
||||
/// crop exactly 172×96 out of the 256×128 art — the frame's own size, a complete panel drawn 1:1
|
||||
/// over it). Our sized-anchor-less fallback draws CENTERED at the authored size instead, which
|
||||
/// squashed the dialog to a 32-tall bar: the count and arrows floated above it and Okay/Cancel
|
||||
/// straddled its bottom edge. The plate's authored size is dropped (StackSplit.xml says why); this
|
||||
/// pins the render so the vestigial size cannot come back.
|
||||
/// over it). 1308 first dodged this by dropping the size; 1310 then landed the byte-verified law
|
||||
/// (an anchor-less region gets an implicit SetAllPoints at creation, size unread under the two
|
||||
/// corners) and restored the ref's own text — this pins the render through the real mechanism.
|
||||
#[test]
|
||||
fn the_split_frame_plate_fills_the_dialog() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
@@ -1057,6 +1056,132 @@ fn the_split_frame_plate_fills_the_dialog() {
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// B180's Bagnon follow-on (director, 08-14): the dialog opened UNDER an addon bag window —
|
||||
/// Bagnon's windows are `frameStrata="HIGH"`, the dialog's own stratum, and our re-expression had
|
||||
/// dropped the reference's `toplevel="true"`, so the dialog's Show raised nothing and lost the
|
||||
/// level tie to the later-shown window (its child buttons at level+1 poked through; the plate did
|
||||
/// not). With the ref's attrs restored, Show runs the verified raise (toplevel.rs: compact, then
|
||||
/// top-occupied-plus-one) and the whole dialog lands above. The synthetic window stands in for
|
||||
/// Bagnon: same stratum, shown after load, overlapping the dialog.
|
||||
#[test]
|
||||
fn the_split_frame_raises_over_a_same_stratum_window() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
let (x, y) = open_backpack_with_a_five_stack(&mut s);
|
||||
|
||||
s.run(
|
||||
r#"
|
||||
local w = CreateFrame("Frame", "FakeBagnon", UIParent)
|
||||
w:SetFrameStrata("HIGH")
|
||||
w:SetPoint("BOTTOMLEFT", 0, 0)
|
||||
w:SetSize(1024, 768)
|
||||
local bg = w:CreateTexture(nil, "BACKGROUND")
|
||||
bg:SetTexture("Interface\\FakeBagnonBG")
|
||||
bg:SetAllPoints()
|
||||
w:Show()
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
|
||||
s.set_modifiers(true, false, false);
|
||||
s.mouse_button(x, y, "LeftButton", true);
|
||||
s.mouse_button(x, y, "LeftButton", false);
|
||||
s.set_modifiers(false, false, false);
|
||||
s.resolve();
|
||||
|
||||
assert!(
|
||||
s.eval::<bool>("return StackSplitFrame:IsShown()").unwrap(),
|
||||
"the spinner opened"
|
||||
);
|
||||
let (dialog, bagnon) = s
|
||||
.eval::<(i64, i64)>("return StackSplitFrame:GetFrameLevel(), FakeBagnon:GetFrameLevel()")
|
||||
.unwrap();
|
||||
assert!(
|
||||
dialog > bagnon,
|
||||
"Show must raise the toplevel dialog over the same-stratum window \
|
||||
(dialog level {dialog}, window level {bagnon})"
|
||||
);
|
||||
// The symptom itself: the plate paints AFTER the window's background in draw order.
|
||||
let order: Vec<String> = s
|
||||
.extract()
|
||||
.iter()
|
||||
.filter_map(|q| match &q.content {
|
||||
QuadContent::Texture { path: Some(p), .. }
|
||||
if p.contains("FakeBagnonBG") || p.contains("UI-MoneyFrame") =>
|
||||
{
|
||||
Some(p.clone())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(order.len(), 2, "both textures on screen: {order:?}");
|
||||
assert!(
|
||||
order[0].contains("FakeBagnonBG"),
|
||||
"the plate must draw over the window, not under it: {order:?}"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// Typed-digit entry in the split spinner (decision 1319) — the director's ask, and the deferral
|
||||
/// this file's header carried since 0216. Pins the whole chain in one go: the dialog is in the
|
||||
/// keyboard walk (`enableKeyboard`), a digit reaches its `OnChar`, the first digit REPLACES the
|
||||
/// seeded 1 while later digits append, an over-max entry clamps instead of being rejected,
|
||||
/// BACKSPACE drops a digit, and ENTER commits exactly as Okay does.
|
||||
#[test]
|
||||
fn typing_a_number_into_the_split_spinner_sets_the_count() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
let (x, y) = open_backpack_with_a_five_stack(&mut s);
|
||||
|
||||
s.set_modifiers(true, false, false);
|
||||
s.mouse_button(x, y, "LeftButton", true);
|
||||
s.mouse_button(x, y, "LeftButton", false);
|
||||
s.set_modifiers(false, false, false);
|
||||
s.resolve();
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return StackSplitFrame.split").unwrap(),
|
||||
1,
|
||||
"opens seeded at 1"
|
||||
);
|
||||
|
||||
// A digit is consumed by the dialog — which is also what stops it firing action button 3.
|
||||
assert!(s.char_input("3"), "the spinner consumed the digit");
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return StackSplitFrame.split").unwrap(),
|
||||
3,
|
||||
"the first digit REPLACES the seed rather than appending to it"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return StackSplitText:GetText()").unwrap(),
|
||||
"3",
|
||||
"and the label follows"
|
||||
);
|
||||
|
||||
// A second digit appends, then clamps to the 5-stack rather than being thrown away.
|
||||
assert!(s.char_input("7"));
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return StackSplitFrame.split").unwrap(),
|
||||
5,
|
||||
"37 against a 5-stack clamps to 5"
|
||||
);
|
||||
// BACKSPACE drops a digit off the clamped value.
|
||||
assert!(s.frame_key_input("BACKSPACE"), "the dialog took BACKSPACE");
|
||||
assert_eq!(s.eval::<i64>("return StackSplitFrame.split").unwrap(), 1);
|
||||
|
||||
// Type a real value and commit with ENTER — the Okay path, so the carry is picked up.
|
||||
assert!(s.char_input("4"));
|
||||
assert_eq!(s.eval::<i64>("return StackSplitFrame.split").unwrap(), 4);
|
||||
assert!(s.key_input("ENTER"), "ENTER is consumed by the dialog");
|
||||
assert!(
|
||||
!s.eval::<bool>("return StackSplitFrame:IsShown()").unwrap(),
|
||||
"ENTER commits and closes, like Okay"
|
||||
);
|
||||
let held = s.cursor_item().expect("ENTER committed the split");
|
||||
assert_eq!(held.count, Some(4), "the typed count is what got picked up");
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// Okay in the split spinner only picks the split carry up (ref/cursor.rs `SplitContainerItem` —
|
||||
/// a pickup, not a self-contained move); a SUBSEQUENT placement is what actually queues the
|
||||
/// `ContainerMove` with `count: Some(n)`, drained the same way any other container move is.
|
||||
|
||||
@@ -205,6 +205,30 @@ pub(super) fn feed_ui_input(
|
||||
// route law decides; unfocused they return unconsumed, and the camera/turn keys that
|
||||
// read arrows after UiInput see them exactly as before).
|
||||
let chord = keymap::chord(ev.key_code, mods, mac);
|
||||
// A KEYBOARD FRAME gets these by NAME before their chord runs (decision 1319). They are
|
||||
// the keys a focused EditBox receives as a semantic `EditAction` instead of a name, so
|
||||
// they never reached `key_input` at all — and a dialog you type into needs its BACKSPACE.
|
||||
// `frame_key_input` walks in the engine's own order and declines at a focused box, so the
|
||||
// box's editing keys can never be stolen by a frame below it; a `true` here means a frame
|
||||
// consumed the key, which suppresses BOTH the chord below and the key's binding (the
|
||||
// reference's existence gate, wow-re `frame-key-script-delivery.md` §3).
|
||||
let frame_named = match ev.key_code {
|
||||
KeyCode::Backspace => Some("BACKSPACE"),
|
||||
KeyCode::Delete => Some("DELETE"),
|
||||
KeyCode::ArrowLeft => Some("LEFT"),
|
||||
KeyCode::ArrowRight => Some("RIGHT"),
|
||||
KeyCode::ArrowUp => Some("UP"),
|
||||
KeyCode::ArrowDown => Some("DOWN"),
|
||||
KeyCode::Home => Some("HOME"),
|
||||
KeyCode::End => Some("END"),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(name) = frame_named {
|
||||
if script.frame_key_input(name) {
|
||||
capture.0 = true; // consumed by a frame: its binding must not also fire
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if let Some(name) = named {
|
||||
// The three box-event keys. An unconsumed press is not acted on here: every GAME
|
||||
// action of a key — ESCAPE's close/cancel ladder (TOGGLEGAMEMENU), TAB's targeting,
|
||||
@@ -212,7 +236,12 @@ pub(super) fn feed_ui_input(
|
||||
// dispatch (decision 0997, `crate::bindings`), which runs right after this pass and
|
||||
// reads the capture gate written above, so a key a focused box consumed this frame
|
||||
// never also fires its binding — the real client's ESC precedence, table-wide.
|
||||
script.key_input(name);
|
||||
// Consumption suppresses the binding — a keyboard FRAME that took this key counts
|
||||
// exactly as a focused box does (decision 1319; `capture.0` above already covers the
|
||||
// box case, and this adds the frame one).
|
||||
if script.key_input(name) {
|
||||
capture.0 = true;
|
||||
}
|
||||
} else if let Some(chord) = chord {
|
||||
match chord {
|
||||
keymap::Chord::Edit(action) => {
|
||||
@@ -244,7 +273,12 @@ pub(super) fn feed_ui_input(
|
||||
// plane European layouts type real text with (and macOS Option+letter comes through
|
||||
// with only `alt`, composing its special characters).
|
||||
if let Some(text) = &ev.text {
|
||||
script.char_input(text);
|
||||
// Same suppression as the named keys, and this is the case that bites: the number
|
||||
// row IS a binding (the action buttons), so a digit typed into a keyboard frame —
|
||||
// the stack-split spinner — must not also fire action button 3 (decision 1319).
|
||||
if script.char_input(text) {
|
||||
capture.0 = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,10 +184,23 @@ fn the_shipped_pet_bar_drives_end_to_end() {
|
||||
1,
|
||||
"only Claw can autocast"
|
||||
);
|
||||
// 4 emitters x the DERIVED trail length, on the one autocasting slot — and on that slot only,
|
||||
// which is the lazy pool's own pin: the other nine buttons have no sparks at all, where they
|
||||
// used to carry a full hidden set each.
|
||||
let trail: usize = s.eval("return BENILLA_PET_AUTOCAST_TRAIL").unwrap();
|
||||
assert_eq!(
|
||||
trail, 23,
|
||||
"the size ramp's own spacing (see PetActionBar.xml's header) comes out at 23 samples"
|
||||
);
|
||||
assert_eq!(
|
||||
textures(&quads, "Interface\\Buttons\\GlowStar"),
|
||||
32,
|
||||
"4 emitters x 8 trail stars, on the one autocasting slot"
|
||||
4 * trail,
|
||||
"4 emitters x the trail, on the one autocasting slot"
|
||||
);
|
||||
assert!(
|
||||
s.eval::<bool>("return PetActionButton1.sparks == nil")
|
||||
.unwrap(),
|
||||
"a button that never autocasts never pools a spark"
|
||||
);
|
||||
|
||||
// Geometry, quoted from the ref: the bar's TOPLEFT is MainMenuBar's BOTTOMLEFT +(36,97),
|
||||
@@ -631,3 +644,282 @@ fn the_keybind_pair_pushes_and_casts_without_the_clicks_forks() {
|
||||
);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// **B228 — the autocast trail's motion, MEASURED off the shipped handler.**
|
||||
///
|
||||
/// nazriel reads the shine as "a bit too fast". The health pass answered that from the constants —
|
||||
/// the .m2 authors a 2.000 s loop and `BENILLA_PET_AUTOCAST_PERIOD` is 2.0 — which is a read, not a
|
||||
/// measurement: it says nothing about whether the OnUpdate wiring turns wall-clock seconds into
|
||||
/// that period, and an `elapsed` in the wrong unit would be invisible to it. So this drives the
|
||||
/// real `<OnUpdate>` with real seconds and reads the head spark's own SetPoint back.
|
||||
///
|
||||
/// What is pinned, all of it off the file (`m2bones` on `Interface\Buttons\UI-AutoCastButton.m2`):
|
||||
/// a 28.8 px square (0.02 units × 1200 px/unit × the ref Model's 1.2), one lap per 2.000 s, a
|
||||
/// corner every 0.500 s, LINEAR between them (`interp == 1` at the bone record's translation
|
||||
/// M2Track — so the half-edge sample sits at the half-time, no easing), and the four emitters one
|
||||
/// quarter-lap apart. If the reported overspeed is in the period, it fails here; if this holds and
|
||||
/// the report stands, the period is exonerated and the gap is in the trail's SHAPE.
|
||||
#[test]
|
||||
fn the_autocast_trail_laps_the_rim_once_every_two_seconds() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_pet_bar(&s);
|
||||
declare_token_strings(&s);
|
||||
s.set_pet_actions(true, true, true, hunter_slots());
|
||||
s.fire_event("PET_BAR_UPDATE", vec![]);
|
||||
s.resolve();
|
||||
|
||||
// Slot 4 is Claw, the one slot in the fixture with autocast RUNNING. `sparks[n]` is the pool
|
||||
// `BenillaPetAutocast_Create` built: emitter-major, so emitter e's HEAD (age 0, the bone's own
|
||||
// position) is index e·TRAIL + 1.
|
||||
let head = |s: &UiScript, emitter: usize| -> (f32, f32) {
|
||||
let expr = |axis: usize| {
|
||||
format!(
|
||||
"local n = {emitter} * BENILLA_PET_AUTOCAST_TRAIL + 1 \
|
||||
local a, b, c, x, y = PetActionButton4.sparks[n]:GetPoint() \
|
||||
return ({axis} == 0) and x or y"
|
||||
)
|
||||
};
|
||||
(
|
||||
s.eval::<f32>(&expr(0)).unwrap(),
|
||||
s.eval::<f32>(&expr(1)).unwrap(),
|
||||
)
|
||||
};
|
||||
let near = |got: (f32, f32), want: (f32, f32), what: &str| {
|
||||
assert!(
|
||||
(got.0 - want.0).abs() < 0.01 && (got.1 - want.1).abs() < 0.01,
|
||||
"{what}: at {got:?}, expected {want:?}"
|
||||
);
|
||||
};
|
||||
|
||||
// 0.02 units × 1200 px/unit × 1.2 = 28.8 px across, so the corners sit ±14.4 from centre.
|
||||
const H: f32 = 14.4;
|
||||
let (bl, tl, tr, br) = ((-H, -H), (-H, H), (H, H), (H, -H));
|
||||
|
||||
// ── The clock WIRING: four half-second ticks of the shipped OnUpdate, one corner each. This
|
||||
// is the half a constants read cannot reach — `arg1` really is seconds, and the bar's clock
|
||||
// advances 1:1 with it.
|
||||
let corner = |s: &mut UiScript, dt: f32, want: (f32, f32), what: &str| {
|
||||
s.tick(dt);
|
||||
near(head(s, 0), want, what);
|
||||
};
|
||||
corner(
|
||||
&mut s,
|
||||
0.5,
|
||||
tl,
|
||||
"0.5 s in: one edge, at the top-left corner",
|
||||
);
|
||||
corner(&mut s, 0.5, tr, "1.0 s: half a lap");
|
||||
corner(&mut s, 0.5, br, "1.5 s: three quarters");
|
||||
corner(
|
||||
&mut s,
|
||||
0.5,
|
||||
bl,
|
||||
"2.0 s: exactly one lap, back where it started",
|
||||
);
|
||||
|
||||
// ── The position LAW, driven at exact clocks so no tick accumulation can blur it. The
|
||||
// mid-edge sample is the `interp == 1` pin: linear means the half-time is the half-edge, and
|
||||
// a spline-eased track would sit short of it.
|
||||
let at = |s: &UiScript, clock: f32| -> (f32, f32) {
|
||||
s.run(&format!(
|
||||
"BenillaPetAutocast_Update(PetActionButton4, {clock})"
|
||||
))
|
||||
.unwrap();
|
||||
head(s, 0)
|
||||
};
|
||||
near(at(&s, 0.0), bl, "clock 0");
|
||||
near(
|
||||
at(&s, 0.25),
|
||||
(-H, 0.0),
|
||||
"quarter edge in: the LEFT edge's midpoint",
|
||||
);
|
||||
near(at(&s, 0.5), tl, "clock 0.5");
|
||||
near(at(&s, 1.0), tr, "clock 1.0");
|
||||
near(at(&s, 1.5), br, "clock 1.5");
|
||||
near(at(&s, 2.0), bl, "clock 2.0 wraps onto clock 0");
|
||||
|
||||
// ── The trail is CONTINUOUS, which is what deriving the sample count buys (B228). No two
|
||||
// neighbours in a trail sit further apart than the smaller of their two stars is wide, so the
|
||||
// streak has no seam at any age. The 8-even-samples version failed this from mid-life back —
|
||||
// 7.2 px apart with 4.3 px stars — and a row of separated dots is what reads as marching.
|
||||
let worst_gap: f32 = s
|
||||
.eval(
|
||||
"local worst = 0 \
|
||||
for k = 1, BENILLA_PET_AUTOCAST_TRAIL - 1 do \
|
||||
local a, b = PetActionButton4.sparks[k], PetActionButton4.sparks[k + 1] \
|
||||
local _, _, _, ax, ay = a:GetPoint() \
|
||||
local _, _, _, bx, by = b:GetPoint() \
|
||||
local d = math.sqrt((ax - bx) * (ax - bx) + (ay - by) * (ay - by)) \
|
||||
local w = math.min(a:GetWidth(), b:GetWidth()) \
|
||||
if d / w > worst then worst = d / w end \
|
||||
end \
|
||||
return worst",
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
worst_gap <= 1.0,
|
||||
"the widest neighbour gap is {worst_gap:.2} star-widths — the streak has a seam"
|
||||
);
|
||||
|
||||
// ── The four emitters are ONE LAP, a quarter apart — the file's four bones at the four
|
||||
// corners, chasing each other (m2bones: pivots at (-0.001,0), (0.019,0), (0.019,0.02),
|
||||
// (-0.001,0.02), same track, one corner of phase between neighbours).
|
||||
s.run("BenillaPetAutocast_Update(PetActionButton4, 0.0)")
|
||||
.unwrap();
|
||||
for (e, want, name) in [
|
||||
(0, bl, "emitter 0"),
|
||||
(1, tl, "emitter 1"),
|
||||
(2, tr, "emitter 2"),
|
||||
(3, br, "emitter 3"),
|
||||
] {
|
||||
near(head(&s, e), want, name);
|
||||
}
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// The pet bar plus the three files its TOOLTIP needs, in the TOC's own order: Fonts.xml (the
|
||||
/// colour codes that wrap the binding), GameTooltip.xml (the plate), and UIParent.xml — already a
|
||||
/// prerequisite for the managed stack — for `GetBindingText`, which renders the key.
|
||||
fn load_pet_bar_with_tooltip(s: &UiScript) {
|
||||
for file in [
|
||||
"Fonts.xml",
|
||||
"UiPanels.xml",
|
||||
"UIParent.xml",
|
||||
"GameTooltip.xml",
|
||||
"Cooldown.xml",
|
||||
"ActionBar.xml",
|
||||
"PetActionBar.xml",
|
||||
] {
|
||||
load_xml(s, file);
|
||||
}
|
||||
}
|
||||
|
||||
/// A hunter bar hovered through the REAL gesture, with the real binding registry and the real CVar
|
||||
/// table behind it — `mouse_move` runs the shipped `<OnEnter>` with `this` bound, which is the only
|
||||
/// way this fork gets exercised the way nazriel exercised it.
|
||||
///
|
||||
/// Buttons are 30 px chained +8 from the bar's own origin: button 1 spans x[72,102] y[56,86] ⇒
|
||||
/// centre (87,71), button 4's left = 72 + 3·38 ⇒ centre (201,71) — the geometry
|
||||
/// `clicks_route_through_the_attack_toggle_fork` already leans on.
|
||||
const ATTACK_BUTTON: (f32, f32) = (87.0, 71.0);
|
||||
const CLAW_BUTTON: (f32, f32) = (201.0, 71.0);
|
||||
|
||||
fn hovered_pet_bar() -> UiScript {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
// The real command set and the real registered defaults, the way the app's own seed installs
|
||||
// them — so `GetBindingKey("BONUSACTIONBUTTON1")` answers CTRL-1 (its byte-real default) and
|
||||
// `GetCVar("UberTooltips")` answers "1" (its byte-read registrar value) rather than nil.
|
||||
s.register_bindings(&crate::bindings::registry_commands());
|
||||
s.register_cvars(crate::cvars::REGISTERED.iter().copied());
|
||||
load_pet_bar_with_tooltip(&s);
|
||||
declare_token_strings(&s);
|
||||
s.set_pet_actions(true, true, true, hunter_slots());
|
||||
s.fire_event("PET_BAR_UPDATE", vec![]);
|
||||
s.resolve();
|
||||
s
|
||||
}
|
||||
|
||||
fn tooltip_line1(s: &UiScript) -> String {
|
||||
s.eval::<String>("return GameTooltipTextLeft1:GetText() or \"\"")
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// **B230 — Attack/Follow/Stay name their keybinding; the pet's own spells do not.**
|
||||
///
|
||||
/// nazriel: *"On 1.12 when you hover over Attack, Follow, Stay pet action buttons they show the
|
||||
/// keybinding - which is not the case with Benilla"*. The reference agrees with him and is
|
||||
/// narrower than "the pet bar": `PetActionButton_OnEnter` (PetActionBarFrame.lua l.285-305) forks
|
||||
/// on `isToken or UberTooltips == "0"`, and only THAT branch concatenates
|
||||
/// `NORMAL_FONT_COLOR_CODE.." ("..GetBindingText(GetBindingKey("BONUSACTIONBUTTON"..id), "KEY_")..")"`.
|
||||
/// The other branch is a bare `SetPetAction` and appends nothing — so a fix that suffixed every
|
||||
/// slot would be wrong, and the control below is what says ours doesn't.
|
||||
///
|
||||
/// The exact string is pinned, colour codes and all, because the ref's concatenation puts the
|
||||
/// colour BEFORE the space (`Attack|cffffd200 (CTRL-1)|r`, not `Attack |cffffd200(CTRL-1)|r` —
|
||||
/// which is what MicroMenu.xml's different-shaped version of the same idea produces).
|
||||
#[test]
|
||||
fn token_tooltips_name_their_binding_and_pet_spells_do_not() {
|
||||
let mut s = hovered_pet_bar();
|
||||
|
||||
s.mouse_move(ATTACK_BUTTON.0, ATTACK_BUTTON.1);
|
||||
s.resolve();
|
||||
assert_eq!(
|
||||
tooltip_line1(&s),
|
||||
"Attack|cffffd200 (CTRL-1)|r",
|
||||
"a command token carries BONUSACTIONBUTTON1's key in the normal-font colour"
|
||||
);
|
||||
// Uber on + a token ⇒ still the default corner; only the CVar-off leg owner-anchors.
|
||||
assert!(
|
||||
s.eval::<bool>("return GameTooltip.default ~= nil").unwrap(),
|
||||
"a token's plate takes the default corner while UberTooltips is on"
|
||||
);
|
||||
|
||||
// The CONTROL: slot 4 is Claw, a real pet spell, and it goes through the engine channel —
|
||||
// its name, and nothing in parentheses.
|
||||
s.mouse_move(CLAW_BUTTON.0, CLAW_BUTTON.1);
|
||||
s.resolve();
|
||||
let claw = tooltip_line1(&s);
|
||||
assert_eq!(claw, "Claw", "a pet SPELL renders through SetPetAction");
|
||||
assert!(
|
||||
!claw.contains('('),
|
||||
"…and appends no binding: {claw:?} (the ref's other branch)"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// The append is UNCONDITIONAL in that branch, so an unbound row really does render empty
|
||||
/// parentheses — `GetBindingText` answers `""` for a nil key (UIParent.xml, transcribed from the
|
||||
/// ref's own l.1819-1821), and nothing guards the `"("`. That is the reference's behaviour, not a
|
||||
/// slip of ours: MicroButtonTooltipText guards its own suffix and this one does not.
|
||||
///
|
||||
/// Stock it is unreachable — BONUSACTIONBUTTON1-10 default to CTRL-1..CTRL-0 — so it takes a
|
||||
/// deliberate unbind to see, which is exactly what this does.
|
||||
#[test]
|
||||
fn an_unbound_token_row_renders_the_references_empty_parentheses() {
|
||||
let mut s = hovered_pet_bar();
|
||||
s.run(r#"SetBinding("CTRL-1")"#).unwrap();
|
||||
assert!(
|
||||
s.eval::<Option<String>>(r#"return GetBindingKey("BONUSACTIONBUTTON1")"#)
|
||||
.unwrap()
|
||||
.is_none(),
|
||||
"the row really is unbound now"
|
||||
);
|
||||
|
||||
s.mouse_move(ATTACK_BUTTON.0, ATTACK_BUTTON.1);
|
||||
s.resolve();
|
||||
assert_eq!(
|
||||
tooltip_line1(&s),
|
||||
"Attack|cffffd200 ()|r",
|
||||
"unbound: the parentheses stay, empty — the ref's own unguarded concatenation"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// The CVar is the fork's OTHER half, and it is not decoration: with `UberTooltips` off, a real pet
|
||||
/// SPELL stops going through the engine channel and takes the token branch too — same built text,
|
||||
/// same binding suffix — and the plate moves from the screen corner to beside the button
|
||||
/// (`SetOwner(this, "ANCHOR_RIGHT")`). Both are the reference's, in the same five lines.
|
||||
///
|
||||
/// This is also the row's reason to exist in `cvars::REGISTERED`: benilla registers `UberTooltips`
|
||||
/// at all because these Lua sites read it (the honest-tree rule's live-consumer half).
|
||||
#[test]
|
||||
fn ubertooltips_off_takes_the_spells_through_the_token_branch_and_moves_the_plate() {
|
||||
let mut s = hovered_pet_bar();
|
||||
s.set_cvar_engine("UberTooltips", "0");
|
||||
|
||||
s.mouse_move(CLAW_BUTTON.0, CLAW_BUTTON.1);
|
||||
s.resolve();
|
||||
assert_eq!(
|
||||
tooltip_line1(&s),
|
||||
"Claw|cffffd200 (CTRL-4)|r",
|
||||
"with the CVar off a pet spell is built here too, binding and all"
|
||||
);
|
||||
assert!(
|
||||
s.eval::<bool>("return GameTooltip:IsOwned(PetActionButton4)")
|
||||
.unwrap(),
|
||||
"…and the plate sits beside the button, not at the screen corner"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
@@ -192,11 +192,16 @@ fn your_own_portrait_explains_the_menu_instead_of_showing_your_health() {
|
||||
|
||||
/// Action-bar hovers take the default corner too — ref ActionButton_SetTooltip l.366-372
|
||||
/// branches on the UberTooltips CVar, whose stock default is "1" (byte-read from WoW.exe
|
||||
/// 0x48fdd9 / default string 0x82e748; see ActionBar.xml). An empty slot renders nothing,
|
||||
/// 0x48fdd9 / default string 0x82e748; see `cvars::REGISTERED`). An empty slot renders nothing,
|
||||
/// but the anchor must already be seated — asserted through GetPoint, resolved rect or not.
|
||||
///
|
||||
/// Registered, not merely absent: before B230 the CVar was not in the table at all, so `GetCVar`
|
||||
/// answered nil and the "1" leg was reached by accident rather than by value. This seeds the real
|
||||
/// table so the pass means what it says.
|
||||
#[test]
|
||||
fn action_button_hover_takes_the_default_corner() {
|
||||
let s = harness(&["Cooldown.xml", "ActionBar.xml"]);
|
||||
let mut s = harness(&["Cooldown.xml", "ActionBar.xml"]);
|
||||
s.register_cvars(crate::cvars::REGISTERED.iter().copied());
|
||||
s.run("BenillaActionButton_OnEnter(ActionButton3)").unwrap();
|
||||
assert!(s.errors().is_empty(), "hover errors: {:?}", s.errors());
|
||||
let ok: bool = s
|
||||
@@ -212,6 +217,105 @@ fn action_button_hover_takes_the_default_corner() {
|
||||
);
|
||||
}
|
||||
|
||||
/// The other leg of that same branch, live since B230 registered the CVar: with `UberTooltips`
|
||||
/// off, an action button's plate leaves the screen corner and seats BESIDE the button — LEFT for
|
||||
/// the bars the reference lists (`MultiBarBottomRight` is the only one benilla ships; the two
|
||||
/// vertical bars are not), RIGHT for everything else, including the main bar.
|
||||
///
|
||||
/// The two anchors are read off the resolved SetPoint pair, which is what `SetOwner` actually
|
||||
/// writes: ANCHOR_RIGHT = the plate's BOTTOMLEFT on the button's TOPRIGHT, ANCHOR_LEFT its mirror
|
||||
/// (`script/tooltip/verbs.rs`).
|
||||
#[test]
|
||||
fn ubertooltips_off_seats_action_bar_plates_beside_the_button() {
|
||||
let mut s = harness(&["Cooldown.xml", "ActionBar.xml", "MultiBars.xml"]);
|
||||
s.register_cvars(crate::cvars::REGISTERED.iter().copied());
|
||||
s.set_cvar_engine("UberTooltips", "0");
|
||||
|
||||
let seat = |s: &UiScript| {
|
||||
s.eval::<String>(
|
||||
"local p, rel, rp = GameTooltip:GetPoint() \
|
||||
return p .. \"/\" .. (rel and rel:GetName() or \"?\") .. \"/\" .. rp",
|
||||
)
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
s.run("BenillaActionButton_OnEnter(ActionButton3)").unwrap();
|
||||
assert!(
|
||||
s.eval::<bool>("return GameTooltip.default == nil").unwrap(),
|
||||
"off: the main bar's plate is owner-anchored, not the default corner"
|
||||
);
|
||||
assert!(
|
||||
s.eval::<bool>("return GameTooltip:IsOwned(ActionButton3)")
|
||||
.unwrap(),
|
||||
"…owned by the button it opened from"
|
||||
);
|
||||
assert_eq!(
|
||||
seat(&s),
|
||||
"BOTTOMLEFT/ActionButton3/TOPRIGHT",
|
||||
"ANCHOR_RIGHT — the main bar is not in the ref's LEFT set"
|
||||
);
|
||||
|
||||
// MultiBarBottomRight IS in that set — the one shipped member of it.
|
||||
s.run("BenillaActionButton_OnEnter(MultiBarBottomRightButton1)")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
seat(&s),
|
||||
"BOTTOMRIGHT/MultiBarBottomRightButton1/TOPLEFT",
|
||||
"ANCHOR_LEFT — MultiBarBottomRight opens toward screen centre"
|
||||
);
|
||||
|
||||
// And the CVar back on restores the corner — the fork is a fork, not a one-way door.
|
||||
s.set_cvar_engine("UberTooltips", "1");
|
||||
s.run("BenillaActionButton_OnEnter(MultiBarBottomRightButton1)")
|
||||
.unwrap();
|
||||
assert!(
|
||||
s.eval::<bool>("return GameTooltip.default ~= nil").unwrap(),
|
||||
"on: back to the screen corner"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "hover errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// The stance bar's own leg of the same branch (ref BonusActionBarFrame.xml l.40-45 — ANCHOR_RIGHT
|
||||
/// with no bar fork of its own), pinned separately because it is a different file's handler and a
|
||||
/// different tooltip verb; it was collapsed to the "1" leg alongside the action bar's and
|
||||
/// un-collapsed with it.
|
||||
#[test]
|
||||
fn ubertooltips_off_seats_stance_plates_beside_the_button() {
|
||||
let mut s = harness(&["Cooldown.xml", "ActionBar.xml", "StanceBar.xml"]);
|
||||
s.register_cvars(crate::cvars::REGISTERED.iter().copied());
|
||||
s.set_shapeshift_forms(vec![benilla_ui::script::ShapeshiftFormView {
|
||||
spell_id: 5487,
|
||||
name: "Bear Form".into(),
|
||||
texture: Some("Interface\\Icons\\Ability_Racial_BearForm".into()),
|
||||
active: false,
|
||||
castable: true,
|
||||
cooldown: None,
|
||||
}]);
|
||||
s.fire_event("UPDATE_SHAPESHIFT_FORMS", vec![]);
|
||||
s.resolve();
|
||||
|
||||
s.run("BenillaShapeshiftButton_OnEnter(ShapeshiftButton1)")
|
||||
.unwrap();
|
||||
assert!(
|
||||
s.eval::<bool>("return GameTooltip.default ~= nil").unwrap(),
|
||||
"on (the stock default): the screen corner"
|
||||
);
|
||||
|
||||
s.set_cvar_engine("UberTooltips", "0");
|
||||
s.run("BenillaShapeshiftButton_OnEnter(ShapeshiftButton1)")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<String>(
|
||||
"local p, rel, rp = GameTooltip:GetPoint() \
|
||||
return p .. \"/\" .. (rel and rel:GetName() or \"?\") .. \"/\" .. rp",
|
||||
)
|
||||
.unwrap(),
|
||||
"BOTTOMLEFT/ShapeshiftButton1/TOPRIGHT",
|
||||
"off: ANCHOR_RIGHT, beside the button"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "hover errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// Buff hovers hang BELOW the button — ref BuffFrame.xml l.37 is ANCHOR_BOTTOMLEFT (the buff
|
||||
/// row lives at the screen's top-right): the tooltip's TOPRIGHT seats on the button's
|
||||
/// BOTTOMLEFT.
|
||||
|
||||
Reference in New Issue
Block a user