- the action-bar cluster is the reference's own: `MainMenuBar.xml`, `ActionBarFrame.xml`, `MultiActionBars.xml` and `BonusActionBarFrame.xml` run off the player's own chain, and our `ActionBar.xml` (2047 lines), `MultiBars.xml` and `StanceBar.xml` are deleted — three, since the stock bonus-bar file declares the shapeshift bar too; one engine verb, `ChangeActionBarPage`, fires `ACTIONBAR_PAGE_CHANGED` and nothing else; one load blocker: stock `MultiActionBars.lua` writes the options window's table from its OnLoad, the reference's own load-order hack, so our options window moves to the reference's toc slot; and ten things the reference does that ours did not: an empty button is HIDDEN while showgrid is 0, the bonus overlay sits one pixel right of the main buttons, every slide takes one frame more than its time, and `ActionButton_ShowGrid` ghosts EVERY ring, occupied included (1938) - a gift can be wrapped: right-clicking wrapping paper fell through to plain use and sent `CMSG_USE_ITEM`, so nothing happened — you could receive a gift and never give one; the wrap is a displayed-cursor MODE plus a stashed slot, never a payload, so `CursorHasItem()` stays false and every stock gate on it stays shut: right-click arms it, a left-click on a bag slot sends `CMSG_WRAP_ITEM`, and the client applies no eligibility test — the server's `ERR_CANT_WRAP_*` reasons are the error line (1934); a duration-limited item counts down: `SMSG_ITEM_TIME_UPDATE` parks a deadline and the tooltip's `ITEM_DURATION` line runs the enchant countdown's bucket ladder, so "Duration: 1 days" is what the reference renders; an update for an item we do not hold is DROPPED, no queue (1933); and a renamed crowd-control refusal names its mechanic — "Can't do that while stunned" — off `SpellMechanic.dbc` (1943) - a creature paths on a boat: `SMSG_MONSTER_MOVE_TRANSPORT` carries the transport's guid and deck-local coordinates, and is the attach/detach edge; a deck sample lands on the rider or nowhere, never as a world write a few yards from the map origin; and "there is no terrain under a boat" turned out wrong: the ground probe admits GameObject meshes — a boat's `.wmo` is a GameObject, so the probe hits the deck (1936); `MSG_MOVE_TIME_SKIPPED` is read, not dropped — the reference adds the lag to the observed unit's last-wire stamp, so dropping it scheduled their next packet `lag` ms late, a hitch for every player who stalled zoning in near us; and our own settle hold reports once (1935); and the tradeskill tool order was backwards — focus FIRST, so every two-tool recipe had read "Blacksmith Hammer, Anvil" for the reference's "Anvil, Blacksmith Hammer"; the focus's `hasTool` is 1.0 in the reference too (1942) - the crowd's second sitting, instruments first: the probe line carries the tail — cpu p95/p99 and the six worst frames — because a mean under budget hides the one frame in twenty over it (raid legs railed at 16.67 with p95 at 18–20 ms) and whether that frame was work or a wait is the first question about a stutter, and the visible meshes are counted by lane; then two cuts — a terrain tile furnishes as 4×4 cells of 4×4 chunks, one mesh, one entity, one draw per 133-yd cell instead of per 33-yd chunk (a city horizon is over a thousand chunks a frame, and a cell changes nothing a pixel can see; Stormwind 10.55 → 10.32 `cpu_ms`), and an authored point light is our own `WorldPointLight`, not Bevy's `PointLight`, which was only a data carrier while Bevy ran its whole light lane over every lamp, ~2 % of the alone frame; the sitting's record is 1945, and the code says so (1945) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
589 lines
25 KiB
Rust
589 lines
25 KiB
Rust
//! The target-of-target frame, driven end to end (decision 1576) — the **reference's own**
|
|
//! `TargetofTargetFrame`, declared in `Interface\FrameXML\TargetFrame.xml` (l.515-680) and
|
|
//! driven by `TargetFrame.lua`, off the player's patch chain. Decision 1751 retired our
|
|
//! `assets/ui/UnitFrames.xml` transcription, so what is under test here is the stock file over
|
|
//! synthetic `"targettarget"` snapshots.
|
|
//!
|
|
//! Three things here are not ordinary unit-frame plumbing and are what most of these test:
|
|
//!
|
|
//! * **The visibility law is six gates deep** — a switch, five display modes, and the four unit
|
|
//! tests the reference wraps them in — and a frame that gets any of them wrong is either always
|
|
//! there or never there. The mode leg leaks by construction: its solo arm calls neither `Show`
|
|
//! nor `Hide` when you are in a raid (ref TargetFrame.lua l.513-520), so the frame keeps
|
|
//! whatever state it had. Our transcription closed that; the stock file is the end state, so the
|
|
//! leak is now the behaviour under test.
|
|
//! * **Nothing on this frame is event-driven.** Its bars carry no `OnEvent` at all (ref
|
|
//! TargetFrame.xml l.593-625 — the health bar has only `OnValueChanged`, the mana bar no
|
|
//! `<Scripts>` block), so the `UNIT_HEALTH`/`UNIT_MANA` registrations
|
|
//! `UnitFrameHealthBar_Initialize`/`UnitFrameManaBar_Initialize` make (ref UnitFrame.lua
|
|
//! l.150-151, l.190-200) are dead, and so is the frame's own `UNIT_AURA` registration (ref
|
|
//! TargetFrame.xml l.668) — `UnitFrame_OnEvent` handles three events and that is not one of
|
|
//! them. `TargetofTarget_OnUpdate` → `TargetofTarget_Update` is the only driver of the name, the
|
|
//! bars, the dead word, the portrait tint and the aura row. **That is why nearly every step in
|
|
//! this file is a `tick`, not a `fire_event`.**
|
|
//! * **A token going away is silent.** The feed clears `"targettarget"` without an event, by the
|
|
//! same convention `"target"` uses, so the frame's own events cannot take it down — that is what
|
|
//! `TargetFrame_OnUpdate`'s one-compare reconcile is for, and it has a test.
|
|
|
|
use benilla_ui::script::{
|
|
AuraState, PartyMemberInfo, PartyState, QuadContent, RaidMemberInfo, ScriptValue,
|
|
SelectionRequest, UiScript, UnitState,
|
|
};
|
|
|
|
use super::test_ui::load_ui as load_xml;
|
|
|
|
/// A unit snapshot with a guid — the identity `UnitIsUnit` compares, which the "your target is
|
|
/// you" gate reads.
|
|
fn unit(name: &str, guid: u64, health: u32) -> UnitState {
|
|
UnitState {
|
|
exists: true,
|
|
is_connected: true,
|
|
name: Some(name.into()),
|
|
guid,
|
|
health,
|
|
max_health: 100,
|
|
level: 60,
|
|
power_type: 0,
|
|
power: 50,
|
|
max_power: 100,
|
|
..UnitState::default()
|
|
}
|
|
}
|
|
|
|
/// The frame's production load prefix, plus a player, a target and a target's target — the state
|
|
/// in which everything but the switch itself already allows the frame.
|
|
///
|
|
/// The tail is the target-aura tests' set and is here for the same reasons: `BuffFrame` defines
|
|
/// `DebuffTypeColor`, which the debuff row's dispel tint indexes, and it will not TICK without
|
|
/// `ActionBar`'s `TOOLTIP_UPDATE_TIME` — which almost every test in this file needs, because the
|
|
/// stock frame is driven from its OnUpdate and nowhere else (see the module doc).
|
|
fn load_tot() -> 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, "UIParent.xml");
|
|
load_xml(&s, "MoneyFrame.xml");
|
|
load_xml(&s, "GameTooltip.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
|
|
load_xml(&s, "UnitPopup.xml");
|
|
load_xml(&s, "Cooldown.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.lua");
|
|
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
|
load_xml(&s, "Interface\\FrameXML\\MainMenuBar.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.lua");
|
|
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\BuffFrame.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\UnitFrame.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\CombatFeedback.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\PlayerFrame.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\PartyFrame.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\TargetFrame.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\PetFrame.xml");
|
|
// The target-of-target pair's home is `OptionsFrame.xml` (the reference keeps them in
|
|
// `UIOptionsFrame.lua`'s uvar defaults block, l.116-119). That file sits at the BOTTOM of our
|
|
// manifest where the reference's sits near the top, so a test that loads the unit kit alone
|
|
// has to state them itself — the same thing this harness already does for `DEAD`.
|
|
s.run(r#"SHOW_TARGET_OF_TARGET = "0" SHOW_TARGET_OF_TARGET_STATE = "5""#)
|
|
.unwrap();
|
|
s.set_unit("player", Some(unit("Tri", 0x100, 100)));
|
|
s.set_unit("target", Some(unit("Kobold Miner", 0x200, 80)));
|
|
s.set_unit("targettarget", Some(unit("Tri", 0x100, 100)));
|
|
s.fire_event("PLAYER_TARGET_CHANGED", vec![]);
|
|
s
|
|
}
|
|
|
|
/// Turn the frame on the way the option row does — the write plus its `applyFunc`.
|
|
fn switch_on(s: &mut UiScript) {
|
|
s.run(r#"SHOW_TARGET_OF_TARGET = "1" this = TargetofTargetFrame TargetofTarget_Update() this = nil"#)
|
|
.unwrap();
|
|
}
|
|
|
|
fn shown(s: &mut UiScript) -> bool {
|
|
s.eval::<bool>("return TargetofTargetFrame:IsShown() and true or false")
|
|
.unwrap()
|
|
}
|
|
|
|
/// Every texture path the UI actually draws this frame (the pet-frame tests' render probe).
|
|
fn drawn(s: &mut UiScript) -> Vec<String> {
|
|
s.resolve();
|
|
s.extract()
|
|
.into_iter()
|
|
.filter_map(|q| match q.content {
|
|
QuadContent::Texture { path: Some(p), .. } => Some(p),
|
|
_ => None,
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn debuff(spell_id: u32, name: &str, debuff_type: Option<&str>) -> AuraState {
|
|
AuraState {
|
|
spell_id,
|
|
name: Some(name.into()),
|
|
icon: Some(format!("Interface\\Icons\\Spell_{spell_id}")),
|
|
count: 1,
|
|
debuff_type: debuff_type.map(str::to_string),
|
|
// No unit but yourself carries a duration on the 1.12 wire (decision 0257 B6).
|
|
duration: 0.0,
|
|
expiration_time: 0.0,
|
|
helpful: false,
|
|
cancelable: false,
|
|
until_cancelled: false,
|
|
channeled: false,
|
|
}
|
|
}
|
|
|
|
/// A party of `n` others; `GetNumPartyMembers` is that list's length.
|
|
fn party(n: usize) -> PartyState {
|
|
PartyState {
|
|
members: (0..n)
|
|
.map(|i| PartyMemberInfo {
|
|
name: format!("Mate{i}"),
|
|
guid: 0x300 + i as u64,
|
|
})
|
|
.collect(),
|
|
..PartyState::default()
|
|
}
|
|
}
|
|
|
|
/// A raid of `n` (the roster INCLUDES the player, per `GetRaidRosterInfo`'s array).
|
|
fn raid(n: usize) -> PartyState {
|
|
PartyState {
|
|
raid: (0..n)
|
|
.map(|i| RaidMemberInfo {
|
|
name: format!("Raider{i}"),
|
|
guid: 0x400 + i as u64,
|
|
..RaidMemberInfo::default()
|
|
})
|
|
.collect(),
|
|
..PartyState::default()
|
|
}
|
|
}
|
|
|
|
/// The switch is the outer gate, and it ships OFF — 1.12's own default (`SHOW_TARGET_OF_TARGET`
|
|
/// = `"0"`, ref UIOptionsFrame.lua l.116). Everything else about the state already allows the
|
|
/// frame, so this is the switch alone.
|
|
#[test]
|
|
fn the_frame_ships_off_and_the_switch_is_what_shows_it() {
|
|
let mut s = load_tot();
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
assert_eq!(
|
|
s.eval::<String>("return SHOW_TARGET_OF_TARGET").unwrap(),
|
|
"0",
|
|
"the shipped default is the reference's: off"
|
|
);
|
|
assert!(
|
|
!shown(&mut s),
|
|
"off means hidden with everything else ready"
|
|
);
|
|
|
|
switch_on(&mut s);
|
|
assert!(shown(&mut s), "the switch alone brings it up");
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
}
|
|
|
|
/// The four unit gates the reference wraps the switch in (ref TargetofTarget_Update l.504): a
|
|
/// target, a target OF it, a target that is not you, and a target that is alive.
|
|
#[test]
|
|
fn the_four_unit_gates_each_take_the_frame_down() {
|
|
let mut s = load_tot();
|
|
switch_on(&mut s);
|
|
assert!(shown(&mut s));
|
|
|
|
// No target of target.
|
|
s.set_unit("targettarget", None);
|
|
s.run("this = TargetofTargetFrame TargetofTarget_Update() this = nil")
|
|
.unwrap();
|
|
assert!(!shown(&mut s), "nothing to show");
|
|
s.set_unit("targettarget", Some(unit("Tri", 0x100, 100)));
|
|
|
|
// No target at all.
|
|
s.set_unit("target", None);
|
|
s.run("this = TargetofTargetFrame TargetofTarget_Update() this = nil")
|
|
.unwrap();
|
|
assert!(!shown(&mut s), "no target, no target's target");
|
|
s.set_unit("target", Some(unit("Kobold Miner", 0x200, 80)));
|
|
|
|
// The target is YOU: its target is your own target, and the frame would restate the frame it
|
|
// hangs off.
|
|
s.set_unit("target", Some(unit("Tri", 0x100, 100)));
|
|
s.run("this = TargetofTargetFrame TargetofTarget_Update() this = nil")
|
|
.unwrap();
|
|
assert!(!shown(&mut s), "self-target: nothing to add");
|
|
s.set_unit("target", Some(unit("Kobold Miner", 0x200, 80)));
|
|
|
|
// A dead target.
|
|
s.set_unit("target", Some(unit("Kobold Miner", 0x200, 0)));
|
|
s.run("this = TargetofTargetFrame TargetofTarget_Update() this = nil")
|
|
.unwrap();
|
|
assert!(!shown(&mut s), "a corpse is fighting nobody");
|
|
|
|
s.set_unit("target", Some(unit("Kobold Miner", 0x200, 80)));
|
|
s.run("this = TargetofTargetFrame TargetofTarget_Update() this = nil")
|
|
.unwrap();
|
|
assert!(shown(&mut s), "and back");
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
}
|
|
|
|
/// The five display modes, each against solo / party / raid.
|
|
///
|
|
/// Mode 3 in a raid is not a rule the ladder states — it is the hole in it. The
|
|
/// `SHOW_TARGET_OF_TARGET_STATE == "3"` arm acts only while `GetNumRaidMembers() == 0` (ref
|
|
/// TargetFrame.lua l.513-520), so in a raid it calls neither Show nor Hide and the frame keeps
|
|
/// whatever it had. It reads `false` here because the preceding `party` step left it hidden, and
|
|
/// that sequencing is the only reason. The leak itself is
|
|
/// [`the_solo_mode_keeps_the_frame_when_a_raid_forms`], which shows the frame first.
|
|
#[test]
|
|
fn the_five_modes_answer_solo_party_and_raid() {
|
|
let mut s = load_tot();
|
|
switch_on(&mut s);
|
|
|
|
let states = [
|
|
("solo", PartyState::default()),
|
|
("party", party(2)),
|
|
("raid", raid(10)),
|
|
];
|
|
// mode -> (solo, party, raid)
|
|
let expected = [
|
|
("1", [false, false, true]), // raid only
|
|
("2", [false, true, false]), // party, and not while raiding
|
|
("3", [true, false, false]), // solo only
|
|
("4", [false, true, true]), // grouped at all
|
|
("5", [true, true, true]), // always
|
|
];
|
|
for (mode, want) in expected {
|
|
for (i, (label, state)) in states.iter().enumerate() {
|
|
s.set_party(state.clone());
|
|
s.run(&format!(
|
|
r#"SHOW_TARGET_OF_TARGET_STATE = "{mode}" this = TargetofTargetFrame TargetofTarget_Update() this = nil"#
|
|
))
|
|
.unwrap();
|
|
assert_eq!(
|
|
shown(&mut s),
|
|
want[i],
|
|
"mode {mode} while {label}: expected shown={}",
|
|
want[i]
|
|
);
|
|
}
|
|
}
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
}
|
|
|
|
/// Mode 3 in a raid, as a STATE question rather than a fresh evaluation — the leaked branch, which
|
|
/// can only be seen by showing the frame first.
|
|
///
|
|
/// **This assertion flipped with 1751.** Our deleted `UnitFrames.xml` answered the question the
|
|
/// option asks and took the frame down; the stock file does not. `TargetofTarget_Update`'s
|
|
/// `SHOW_TARGET_OF_TARGET_STATE == "3"` arm is `if ( GetNumRaidMembers() == 0 ) then … end` with
|
|
/// no `else` (ref TargetFrame.lua l.513-520), so in a raid it reaches neither `Show` nor `Hide`
|
|
/// and the frame keeps whatever it had. The stock file is the end state, so the reference's answer
|
|
/// is the assertion.
|
|
///
|
|
/// Driven by a `tick`, which is the strong form: `TargetofTarget_OnUpdate` re-runs
|
|
/// `TargetofTarget_Update` on **every** frame (ref TargetFrame.xml l.673-675 → TargetFrame.lua
|
|
/// l.500), so this is not one stale evaluation left standing — the reference re-asks the question
|
|
/// sixty times a second and still never takes the frame down.
|
|
#[test]
|
|
fn the_solo_mode_keeps_the_frame_when_a_raid_forms() {
|
|
let mut s = load_tot();
|
|
switch_on(&mut s);
|
|
s.run(r#"SHOW_TARGET_OF_TARGET_STATE = "3" this = TargetofTargetFrame TargetofTarget_Update() this = nil"#)
|
|
.unwrap();
|
|
assert!(shown(&mut s), "solo, in solo mode");
|
|
|
|
s.set_party(raid(10));
|
|
s.tick(0.016);
|
|
assert!(
|
|
shown(&mut s),
|
|
"the raid forms and the reference's solo arm never fires — the frame stays"
|
|
);
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
}
|
|
|
|
/// The paint: name, health, and the powerless unit's mana rail — which the reference **never
|
|
/// hides**.
|
|
///
|
|
/// **Two things flipped with 1751**, and both are the stock file being the end state:
|
|
///
|
|
/// * **The name arrives on a tick, not on the switch.** `TargetofTarget_Update` (ref
|
|
/// TargetFrame.lua l.503-540) touches the bars, the dead word and the aura rows and never the
|
|
/// name; the `SetText` lives in `TargetofTarget_OnUpdate`'s `CURRENT_TARGETTARGET` compare
|
|
/// (l.494-501), off the frame's own OnUpdate. Turning the switch on alone leaves it nil.
|
|
/// * **A powerless unit still gets a rail.** `UnitFrameManaBar_Update` (ref UnitFrame.lua
|
|
/// l.203-224) sets 0..0 and calls neither `Show` nor `Hide` — there is no hide leg anywhere in
|
|
/// the reference's mana path. Our deleted `UnitFrames.xml` hid the bar and this asserted that;
|
|
/// the stock answer is an empty 0/0 rail, still drawn, so that is what is asserted now.
|
|
///
|
|
/// The powerless step is a `tick` for the same reason as everything else here: 1.12 has no
|
|
/// `UNIT_MAXPOWER` event at all (the reference registers `UNIT_MAXMANA`/`RAGE`/`FOCUS`/`ENERGY`/
|
|
/// `HAPPINESS`, ref UnitFrame.lua l.195-199), and this frame's mana bar carries no `OnEvent` to
|
|
/// answer one with if it did (ref TargetFrame.xml l.612-625).
|
|
#[test]
|
|
fn the_frame_paints_its_unit() {
|
|
let mut s = load_tot();
|
|
switch_on(&mut s);
|
|
s.tick(0.016);
|
|
|
|
assert_eq!(
|
|
s.eval::<String>("return TargetofTargetName:GetText()")
|
|
.unwrap(),
|
|
"Tri"
|
|
);
|
|
let (value, max) = s
|
|
.eval::<(f64, f64)>(
|
|
"local _, m = TargetofTargetHealthBar:GetMinMaxValues() \
|
|
return TargetofTargetHealthBar:GetValue(), m",
|
|
)
|
|
.unwrap();
|
|
assert_eq!((value, max), (100.0, 100.0));
|
|
assert!(
|
|
s.eval::<bool>("return TargetofTargetManaBar:IsShown() and true or false")
|
|
.unwrap(),
|
|
"a unit with mana shows its rail"
|
|
);
|
|
|
|
let mut powerless = unit("Skeleton", 0x100, 100);
|
|
powerless.max_power = 0;
|
|
s.set_unit("targettarget", Some(powerless));
|
|
s.tick(0.016);
|
|
assert!(
|
|
s.eval::<bool>("return TargetofTargetManaBar:IsShown() and true or false")
|
|
.unwrap(),
|
|
"no power, and the rail is STILL drawn — the reference has no hide leg"
|
|
);
|
|
assert_eq!(
|
|
s.eval::<(f64, f64)>(
|
|
"local _, m = TargetofTargetManaBar:GetMinMaxValues() \
|
|
return TargetofTargetManaBar:GetValue(), m",
|
|
)
|
|
.unwrap(),
|
|
(0.0, 0.0),
|
|
"it reads 0/0"
|
|
);
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
}
|
|
|
|
/// DEAD over a dimmed trough — and the reference's connected test, which is what tells a corpse
|
|
/// from a linkdead player (both read zero health). Ref `TargetofTarget_CheckDead`, TargetFrame.lua
|
|
/// l.559-567.
|
|
///
|
|
/// Every step is a `tick` rather than a `UNIT_HEALTH`: `TargetofTargetHealthBar` registers that
|
|
/// event through `UnitFrameHealthBar_Initialize` (ref UnitFrame.lua l.150) but declares **no**
|
|
/// `OnEvent` to answer it with (ref TargetFrame.xml l.593-611 — `OnValueChanged` and nothing
|
|
/// else), so the registration is dead and `TargetofTarget_Update` off the frame's OnUpdate is the
|
|
/// only thing that ever calls `TargetofTarget_CheckDead`.
|
|
#[test]
|
|
fn the_dead_word_needs_a_connected_corpse() {
|
|
let mut s = load_tot();
|
|
switch_on(&mut s);
|
|
s.tick(0.016);
|
|
assert!(
|
|
!s.eval::<bool>("return TargetofTargetDeadText:IsShown() and true or false")
|
|
.unwrap(),
|
|
"alive: no word"
|
|
);
|
|
|
|
let mut corpse = unit("Tri", 0x100, 0);
|
|
corpse.dead = true;
|
|
s.set_unit("targettarget", Some(corpse.clone()));
|
|
s.tick(0.016);
|
|
assert!(
|
|
s.eval::<bool>("return TargetofTargetDeadText:IsShown() and true or false")
|
|
.unwrap(),
|
|
"dead: the word"
|
|
);
|
|
|
|
let mut linkdead = corpse;
|
|
linkdead.is_connected = false;
|
|
s.set_unit("targettarget", Some(linkdead));
|
|
s.tick(0.016);
|
|
assert!(
|
|
!s.eval::<bool>("return TargetofTargetDeadText:IsShown() and true or false")
|
|
.unwrap(),
|
|
"disconnected, not dead"
|
|
);
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
}
|
|
|
|
/// The portrait tint (ref `TargetofTargetHealthCheck`, TargetFrame.lua l.569-585), and its
|
|
/// PLAYERS-ONLY gate: a creature's portrait is never tinted, which is why the check has to run off
|
|
/// the bar's own value rather than off the snapshot.
|
|
///
|
|
/// Ticks, not `UNIT_HEALTH`, and here the chain is one link longer than the dead word's: the check
|
|
/// hangs off the health bar's `OnValueChanged` (ref TargetFrame.xml l.604-608), and the only thing
|
|
/// that moves that value is `UnitFrameHealthBar_Update` inside `TargetofTarget_Update` — which is
|
|
/// reached from the frame's OnUpdate and nowhere else.
|
|
#[test]
|
|
fn the_portrait_tints_with_a_players_state_and_never_a_creatures() {
|
|
let mut s = load_tot();
|
|
switch_on(&mut s);
|
|
s.tick(0.016);
|
|
|
|
let mut hurt = unit("Tri", 0x100, 15);
|
|
hurt.is_player = true;
|
|
s.set_unit("targettarget", Some(hurt));
|
|
s.tick(0.016);
|
|
let (r, g, b) = s
|
|
.eval::<(f64, f64, f64)>("return TargetofTargetPortrait:GetVertexColor()")
|
|
.unwrap();
|
|
assert_eq!((r, g, b), (1.0, 0.0, 0.0), "a player under a fifth: red");
|
|
|
|
let mut healthy = unit("Tri", 0x100, 90);
|
|
healthy.is_player = true;
|
|
s.set_unit("targettarget", Some(healthy));
|
|
s.tick(0.016);
|
|
let (r, g, b) = s
|
|
.eval::<(f64, f64, f64)>("return TargetofTargetPortrait:GetVertexColor()")
|
|
.unwrap();
|
|
assert_eq!((r, g, b), (1.0, 1.0, 1.0), "and white again");
|
|
|
|
// A creature at the same 15%: the tint must not move (the check returns before touching it).
|
|
s.set_unit("targettarget", Some(unit("Kobold", 0x500, 15)));
|
|
s.tick(0.016);
|
|
let (r, g, b) = s
|
|
.eval::<(f64, f64, f64)>("return TargetofTargetPortrait:GetVertexColor()")
|
|
.unwrap();
|
|
assert_eq!((r, g, b), (1.0, 1.0, 1.0), "creatures carry no reading");
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
}
|
|
|
|
/// The left click selects — the one leg the target frame's own click does not have, and the whole
|
|
/// reason the frame is clickable.
|
|
#[test]
|
|
fn the_left_click_targets_the_unit() {
|
|
let mut s = load_tot();
|
|
switch_on(&mut s);
|
|
s.run(r#"TargetofTarget_OnClick("LeftButton")"#).unwrap();
|
|
assert_eq!(
|
|
s.take_selection_requests(),
|
|
vec![SelectionRequest::Unit("targettarget".into())]
|
|
);
|
|
|
|
// A right click is not a menu here: the reference gives this frame none.
|
|
s.run(r#"TargetofTarget_OnClick("RightButton")"#).unwrap();
|
|
assert!(s.take_selection_requests().is_empty());
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
}
|
|
|
|
/// The four debuff buttons, dispel-tinted (ref `RefreshBuffs` over `MAX_PARTY_DEBUFFS`,
|
|
/// BuffFrame.lua l.262-313, called from `TargetofTarget_Update` l.538).
|
|
///
|
|
/// The `UNIT_AURA` fire is what the feed really does and is kept for that reason, but it is inert
|
|
/// here: the frame registers the event in its own OnLoad (ref TargetFrame.xml l.668) and answers
|
|
/// it with `UnitFrame_OnEvent`, which handles `UNIT_NAME_UPDATE`, `UNIT_PORTRAIT_UPDATE` and
|
|
/// `UNIT_DISPLAYPOWER` and nothing else (ref UnitFrame.lua l.31-45). A dead registration in the
|
|
/// stock file — the row is redrawn by `RefreshBuffs` off the OnUpdate, so the `tick` is what makes
|
|
/// it appear.
|
|
#[test]
|
|
fn the_debuff_row_draws_what_the_unit_carries() {
|
|
let mut s = load_tot();
|
|
switch_on(&mut s);
|
|
s.set_auras(
|
|
"targettarget",
|
|
Some(vec![
|
|
debuff(1000, "Rend", None),
|
|
debuff(1001, "Curse of Agony", Some("Curse")),
|
|
]),
|
|
);
|
|
s.fire_event("UNIT_AURA", vec![ScriptValue::Str("targettarget".into())]);
|
|
s.tick(0.016);
|
|
|
|
let paths = drawn(&mut s);
|
|
assert!(
|
|
paths.iter().any(|p| p == "Interface\\Icons\\Spell_1000"),
|
|
"the first debuff's icon draws: {paths:?}"
|
|
);
|
|
assert!(
|
|
paths.iter().any(|p| p == "Interface\\Icons\\Spell_1001"),
|
|
"and the second"
|
|
);
|
|
assert!(
|
|
!s.eval::<bool>("return TargetofTargetFrameDebuff3:IsShown() and true or false")
|
|
.unwrap(),
|
|
"the empty slots stay down"
|
|
);
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
}
|
|
|
|
/// The target's OWN aura rows re-wrap around this frame: 5 to a row instead of 6, and the buffs
|
|
/// open under the second debuff row instead of the seventh icon (ref `TargetDebuffButton_Update`,
|
|
/// TargetFrame.lua l.312-338 for the wrap and l.370-385 for the row anchors).
|
|
///
|
|
/// Both directions, and they are not symmetric: the reference re-runs the rows on the way **in**
|
|
/// only. `TargetDebuffButton_Update` is called from inside `TargetofTarget_Update`'s
|
|
/// `if ( TargetofTargetFrame:IsShown() )` (l.532-537), so turning the frame off leaves the rows
|
|
/// wrapped short behind it.
|
|
#[test]
|
|
fn the_target_rows_wrap_short_while_the_frame_stands_beside_them() {
|
|
let mut s = load_tot();
|
|
// A hostile target, so the debuffs lead and the buffs hang off them.
|
|
s.set_auras("target", Some(vec![debuff(2000, "Sunder", None)]));
|
|
s.fire_event("UNIT_AURA", vec![ScriptValue::Str("target".into())]);
|
|
|
|
let anchor = |s: &mut UiScript, frame: &str| -> String {
|
|
s.eval::<String>(&format!(
|
|
"local _, rel = {frame}:GetPoint(1) return rel:GetName()"
|
|
))
|
|
.unwrap()
|
|
};
|
|
assert_eq!(
|
|
anchor(&mut s, "TargetFrameBuff1"),
|
|
"TargetFrameDebuff7",
|
|
"hidden: the sixth icon closes the first row"
|
|
);
|
|
|
|
switch_on(&mut s);
|
|
assert!(shown(&mut s));
|
|
assert_eq!(
|
|
anchor(&mut s, "TargetFrameBuff1"),
|
|
"TargetFrameDebuff6",
|
|
"shown: the row wraps a slot early"
|
|
);
|
|
assert_eq!(
|
|
anchor(&mut s, "TargetFrameDebuff6"),
|
|
"TargetFrameDebuff1",
|
|
"and the sixth icon starts the second row"
|
|
);
|
|
|
|
// Back off — and the rows STAY wrapped short. **This assertion flipped with 1751.**
|
|
// `TargetofTarget_Update` calls `TargetDebuffButton_Update` from inside
|
|
// `if ( TargetofTargetFrame:IsShown() )` (ref TargetFrame.lua l.532-537), so the pass that
|
|
// hides the frame is exactly the pass that does not re-lay the target's rows. They keep the
|
|
// 5-wide wrap until something else runs the update — the next target change or `UNIT_AURA`.
|
|
// Our deleted `UnitFrames.xml` re-laid them on the way out; the stock file is the end state.
|
|
s.run(r#"SHOW_TARGET_OF_TARGET = "0" this = TargetofTargetFrame TargetofTarget_Update() this = nil"#)
|
|
.unwrap();
|
|
assert_eq!(
|
|
anchor(&mut s, "TargetFrameBuff1"),
|
|
"TargetFrameDebuff6",
|
|
"the frame goes and the rows are left wrapped short behind it"
|
|
);
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
}
|
|
|
|
/// The reconcile. A token going away is CLEARED, not announced — no UNIT_* event carries it — so
|
|
/// the frame's own registrations cannot take it down. `TargetFrame_OnUpdate` is what does, and it
|
|
/// runs only while you have a target.
|
|
#[test]
|
|
fn the_reconcile_takes_the_frame_down_when_the_token_goes_silent() {
|
|
let mut s = load_tot();
|
|
switch_on(&mut s);
|
|
assert!(shown(&mut s));
|
|
|
|
// Exactly what the feed does: clear the token, fire nothing.
|
|
s.set_unit("targettarget", None);
|
|
assert!(shown(&mut s), "no event, so nothing has told the frame yet");
|
|
|
|
s.tick(0.016);
|
|
assert!(!shown(&mut s), "the reconcile notices within a frame");
|
|
|
|
// And the other direction: a token appearing DOES fire, but the reconcile covers it anyway.
|
|
s.set_unit("targettarget", Some(unit("Tri", 0x100, 100)));
|
|
s.tick(0.016);
|
|
assert!(shown(&mut s), "and brings it back");
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
}
|