- the buff bar is the reference's own `BuffFrame.xml`, and the swap found an engine defect: an empty `<OnLoad>` in 1.12's XML is a BLANK that takes an inherited handler away again (seven such sites across four stock files), which our loader read as "none given" — so the two enchant slots would have painted the player's first buff in the corner and cancelled it on right-click; the first fix then had to be corrected at the bytes (`SetScript 0x7025c0` tests only NULL and empty, and nothing trims, so `<OnLoad>↵</OnLoad>` compiles to a valid empty function rather than storing nil, which any addon calling `GetScript` can observe); `SHOW_BUFF_DURATIONS` is applied once post-load because 1.12 leaves that to `UIOptionsFrame.lua`, which benilla does not build — without it the bar ships a 10px pitch error on every buff - gossip quests come through the reference's four verbs — `GetGossipAvailableQuests`/`GetGossipActiveQuests` returning (title, level) PAIRS, with the two `Select*` verbs indexing into that list — replacing three invented names that sat on the beyond-1.12 exception list; the quest LEVEL was on the wire, parsed and dropped because the invented verb had nowhere to put it, and the reference's order (available before active, whatever the packet interleaved) comes with them - eight more files off the chain as groundwork: `LocaleProperties.lua` (the gendered/plural `GetText` selector that eleven stock call sites read their words through — fifty lines of FrameXML that looked like an engine binding), `CinematicFrame.xml`, the UIMenu kit, the `UnitFrame.lua`/`CombatFeedback.lua` kits (the only two names blocking the stock player, party, target and pet frames), the money-input kit, and `ItemButtonTemplate.xml` — the base every bag, bank, merchant, loot, trade and mail slot inherits; `GetMerchantItemMaxStack` (on the wire the whole time, dropped) and the two trade item-link verbs join the engine - two instruments for the migration: a gap report that reads a stock window's call sites against a LOADED VM and says which engine verbs it still needs (28 windows need none, the rest name 1–5), and the readiness probe's documented false positive — loading a stock window on top of our same-named one leaves `getglobal(name)` pointing at ours while the stock handlers run against theirs, so a failure inside a colliding name is a question, not an answer Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
133 lines
4.9 KiB
Rust
133 lines
4.9 KiB
Rust
//! The duel UI (decision 0633, DuelFrame.xml): the challenge popup's show/accept/decline, the
|
|
//! out-of-bounds warning's countdown text and its in-bounds dismissal, and the DUEL_FINISHED
|
|
//! sweep — the Lua wiring between the four engine events `ui_duel`'s feed fires and the shared
|
|
//! StaticPopup engine, driven exactly as that feed drives it.
|
|
|
|
use benilla_ui::script::{DuelRequest, ScriptValue, UiScript};
|
|
|
|
use super::test_ui::load_ui as load_xml;
|
|
|
|
fn setup() -> UiScript {
|
|
let mut s = UiScript::new().unwrap();
|
|
s.set_screen_size(1024.0, 768.0);
|
|
load_xml(&s, "Fonts.xml");
|
|
load_xml(&s, "MoneyFrame.xml");
|
|
load_xml(&s, "UiPanels.xml");
|
|
load_xml(&s, "DuelFrame.xml");
|
|
s
|
|
}
|
|
|
|
/// `DUEL_REQUESTED(name)` raises the challenge popup with the challenger's name filled in, and
|
|
/// Accept queues `AcceptDuel()`.
|
|
#[test]
|
|
fn the_challenge_popup_shows_the_name_and_accept_queues_the_accept() {
|
|
let mut s = setup();
|
|
s.fire_event(
|
|
"DUEL_REQUESTED",
|
|
vec![ScriptValue::Str("Onerogue".to_string())],
|
|
);
|
|
assert!(
|
|
s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap(),
|
|
"DUEL_REQUESTED shows the challenge popup"
|
|
);
|
|
assert_eq!(
|
|
s.eval::<String>("return StaticPopup1Text:GetText()")
|
|
.unwrap(),
|
|
"Onerogue has challenged you to a duel.",
|
|
"arg1 fills the GlobalStrings template's %s"
|
|
);
|
|
s.run("StaticPopup_OnClick(StaticPopup1, 1)").unwrap();
|
|
assert_eq!(s.take_duel_requests(), vec![DuelRequest::Accept]);
|
|
assert!(
|
|
!s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap(),
|
|
"accepting closes it"
|
|
);
|
|
}
|
|
|
|
/// Decline queues `CancelDuel()` — and so does ESC, because the entry is `hideOnEscape` and the
|
|
/// engine routes an escape through OnCancel.
|
|
#[test]
|
|
fn decline_and_escape_both_cancel() {
|
|
let mut s = setup();
|
|
s.fire_event("DUEL_REQUESTED", vec![ScriptValue::Str("Twomage".into())]);
|
|
s.run("StaticPopup_OnClick(StaticPopup1, 2)").unwrap();
|
|
assert_eq!(s.take_duel_requests(), vec![DuelRequest::Cancel]);
|
|
|
|
s.fire_event("DUEL_REQUESTED", vec![ScriptValue::Str("Twomage".into())]);
|
|
s.run("ToggleGameMenu()").unwrap();
|
|
assert_eq!(s.take_duel_requests(), vec![DuelRequest::Cancel]);
|
|
assert!(!s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap());
|
|
}
|
|
|
|
/// The out-of-bounds warning is a bare countdown: no buttons, its text rendered by the
|
|
/// StaticPopup engine's per-tick branch, and `DUEL_INBOUNDS` takes it away.
|
|
#[test]
|
|
fn out_of_bounds_counts_down_and_inbounds_dismisses_it() {
|
|
let mut s = setup();
|
|
s.fire_event("DUEL_OUTOFBOUNDS", vec![]);
|
|
assert!(s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap());
|
|
assert!(
|
|
!s.eval::<bool>("return StaticPopup1Button1:IsShown()")
|
|
.unwrap(),
|
|
"a warning, not a question — no buttons"
|
|
);
|
|
s.tick(0.05);
|
|
assert_eq!(
|
|
s.eval::<String>("return StaticPopup1Text:GetText()")
|
|
.unwrap(),
|
|
"Exiting duel area, you will forfeit in 10 seconds.",
|
|
"the engine's countdown branch fills %d %s"
|
|
);
|
|
s.fire_event("DUEL_INBOUNDS", vec![]);
|
|
assert!(
|
|
!s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap(),
|
|
"coming back inside clears the warning"
|
|
);
|
|
assert!(
|
|
s.take_duel_requests().is_empty(),
|
|
"the bounds pair never sends anything"
|
|
);
|
|
}
|
|
|
|
/// `DUEL_FINISHED` sweeps **both** dialogs and sends nothing — the case that matters is a duel
|
|
/// ending while the challenge popup is still up (the other side cancelled first), which must not
|
|
/// leave a stale popup or fire a second cancel.
|
|
#[test]
|
|
fn finishing_sweeps_both_dialogs_silently() {
|
|
let mut s = setup();
|
|
s.fire_event("DUEL_REQUESTED", vec![ScriptValue::Str("Twomage".into())]);
|
|
assert!(s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap());
|
|
s.fire_event("DUEL_FINISHED", vec![]);
|
|
assert!(
|
|
!s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap(),
|
|
"the challenge popup goes away with the duel"
|
|
);
|
|
assert!(
|
|
s.take_duel_requests().is_empty(),
|
|
"a hide is not a decline — the duel is already over"
|
|
);
|
|
|
|
s.fire_event("DUEL_OUTOFBOUNDS", vec![]);
|
|
s.fire_event("DUEL_FINISHED", vec![]);
|
|
assert!(!s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap());
|
|
assert!(s.take_duel_requests().is_empty());
|
|
}
|
|
|
|
/// The four Era globals queue their intents and nothing else — `StartDuel`/`StartDuelUnit` carry
|
|
/// their argument through so the app can resolve it.
|
|
#[test]
|
|
fn the_era_globals_queue_their_intents() {
|
|
let mut s = setup();
|
|
s.run("AcceptDuel(); CancelDuel(); StartDuel('Onerogue'); StartDuelUnit('target')")
|
|
.unwrap();
|
|
assert_eq!(
|
|
s.take_duel_requests(),
|
|
vec![
|
|
DuelRequest::Accept,
|
|
DuelRequest::Cancel,
|
|
DuelRequest::StartByName("Onerogue".into()),
|
|
DuelRequest::StartByUnit("target".into()),
|
|
]
|
|
);
|
|
}
|