- every dialog is the reference's own: `StaticPopup.xml` + `StaticPopup.lua` load off the player's chain, and 0308's engine with its 37 dialogs leaves `UiPanels.xml`; all 76 of the reference's dialogs are the stock table's, 28 of which no file of ours had ever registered (the resurrect family among them); the nine hidden listener frames that existed only because benilla once had no `UIParent_OnEvent` come home to it verbatim from UIParent.lua, and the seven files that were nothing but a dialog and its driver are deleted; what the swap surfaced: the reference capitalises the countdown's unit word ("5 Minutes until release") where ours had lowercased it, and ESC in the typed delete confirm's box is swallowed — the entry names no escape handler; arms for events this engine never fires (PLAYER_SKINNED, TRADE_REQUEST) wait on their feeds rather than being armed blind (1960)
- the social window and the unit menus are the reference's own files: `FriendsFrame.xml` loads off the chain and our transcription is deleted — its twelve dialogs re-homed, `FRIENDLIST_SHOW` fired as the answer to an explicit `ShowFriends()`, and `Get`/`SetLookingForGroup` a client-side flag until their bodies are carved; `UnitPopup.xml` loads off the chain, our row catalogue and its driver frame go, and nine verbs are built for it — `HasFullControl` off the player-control flag the reference reads at `0x51a158`, `UnitPlayerOrPetInParty`/`InRaid` over a new unit owner, and the six pet one-shots, each pressing the bar's own reaction or command slot word through the same latch and `CMSG_PET_ACTION`, confirmed by wow-re's carve; the Guild Leave/Promote confirms 1958 believed nothing registered were our social file's all along, and the duplicate went with it (1958, 1959)
- the trainer loads on demand: our `TrainerFrame.xml` is deleted and no row replaces it — `Blizzard_TrainerUI` loads off the player's own chain through the reference's `ClassTrainerFrame_LoadUI`, and the LoadOnDemand `Blizzard_*` addons are registry rows read off the chain; the timing IS the mechanism: the list builder writes the state-filter mask to 3 on every `SMSG_TRAINER_LIST` (`0x4d75d9`) and the stock addon pushes its saved filters exactly once, on its own ADDON_LOADED — which fires on the session's first trainer, after that packet, so the saved filters win that open and an eager load would never have applied them; the loader holds an anchor to a sibling not built yet and applies it once the subtree exists; and `UNIT_FLAGS` fires only on a change between two snapshots — wow-re's trio found the per-field watch bridge runs no notify pass on the create leg, correcting 1953 (1957)
- the additive window, built and withdrawn the same hour: under `BLEND_ADD` every additive particle draw commutes with every other, so same-key additive draws can be regrouped exactly — built in the effect lane's merge walk and measured on the crowd rig (Stormwind, forty partybots): the adjacency merge already folds about half the effect items and the regroup found two more folds in a frame of ninety-three, so the pre-regroup walk is what runs; what stays: an `fx=` items/merged census on the FPS probe line, a `DRESS_PART` line per drawn part of the self body on the dress census, and the regression the A/B uncovered: 1948's stock chat migration dropped the drain's plain-line sender, so every `WOW_PROBE_CHAT` rig — the crowd rig's forty `.partybot add`s among them — had gone silent on the server; a probe's slash-less line reaches the wire as SAY again (1955)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
432 lines
17 KiB
Rust
432 lines
17 KiB
Rust
//! The durability alert ("armor guy") against the reference's own `DurabilityFrame.xml`/`.lua`,
|
|
//! executed off the player's chain since 1751's fourth window:
|
|
//! the engine's `GetInventoryAlertStatus` statuses (recomputed on every inventory push, a change
|
|
//! firing `UPDATE_INVENTORY_ALERTS`) drive the ref's own SetAlerts law — body pieces show
|
|
//! together when any body region alerts, showSeparate pieces (Weapon/Shield/Ranged) each show
|
|
//! only themselves, the shield glyph swaps for the off-weapon glyph when the off hand holds a
|
|
//! WEAPON, and the whole frame hides at zero alerts.
|
|
|
|
use super::test_ui::load_ui as load_xml;
|
|
use benilla_ui::script::{
|
|
InvSlotView, InventorySlots, ItemTemplateView, QuadContent, ScriptValue, UiScript,
|
|
};
|
|
|
|
fn harness() -> UiScript {
|
|
let mut s = UiScript::new().unwrap();
|
|
s.set_screen_size(1024.0, 768.0);
|
|
for f in [
|
|
"Interface\\FrameXML\\Fonts.xml",
|
|
"MoneyFrame.xml",
|
|
"UiPanels.xml",
|
|
r"Interface\FrameXML\UIPanelTemplates.lua",
|
|
r"Interface\FrameXML\UIPanelTemplates.xml",
|
|
"Interface\\FrameXML\\GlobalStrings.lua",
|
|
"Interface\\FrameXML\\BasicControls.xml",
|
|
"Interface\\FrameXML\\LocaleProperties.lua",
|
|
"Interface\\FrameXML\\StaticPopup.xml",
|
|
"UIParent.xml",
|
|
"GameTooltip.xml",
|
|
"MinimapCluster.xml",
|
|
// The reference's own file. This module carried a private disk-only `load_xml` until the
|
|
// swap, which structurally could not name a chain entry — [`super::test_ui::load_ui`] is
|
|
// the one reader that speaks both stores, and it is why that helper exists.
|
|
"Interface\\FrameXML\\DurabilityFrame.xml",
|
|
] {
|
|
load_xml(&s, f);
|
|
}
|
|
// The app installs this at the end of every real load; a test VM has to say so itself. It is
|
|
// benilla's stated repair of a reference bug — the seat going stale when a side glyph appears
|
|
// while the frame is already shown — and its whole reasoning is at the constant.
|
|
super::manifest::install_durability_reseat(&s).unwrap();
|
|
s
|
|
}
|
|
|
|
/// One equipped slot view with a live durability pair.
|
|
fn slot(item_id: u32, durability: Option<(u32, u32)>) -> Option<InvSlotView> {
|
|
Some(InvSlotView {
|
|
item_id,
|
|
durability,
|
|
count: 1,
|
|
quality: 2,
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
/// The atlas cell (`<TexCoords>`) of a shown region's quad → its painted vertex color; `None`
|
|
/// when no shown quad samples that cell. The weapon and off-weapon glyphs share one cell — in
|
|
/// any single state at most one of the pair is shown, so the cell stays unambiguous.
|
|
fn cell_color(s: &mut UiScript, cell: [f32; 4]) -> Option<[f32; 4]> {
|
|
s.resolve();
|
|
s.extract().iter().find_map(|q| match &q.content {
|
|
QuadContent::Texture {
|
|
tex_coords: Some(tc),
|
|
color,
|
|
..
|
|
} if tc
|
|
.edges()
|
|
.iter()
|
|
.zip(cell)
|
|
.all(|(a, b)| (a - b).abs() < 1e-4) =>
|
|
{
|
|
Some(color.unwrap_or([1.0, 1.0, 1.0, 1.0]))
|
|
}
|
|
_ => None,
|
|
})
|
|
}
|
|
|
|
const HEAD_CELL: [f32; 4] = [0.0, 0.140625, 0.0, 0.171875];
|
|
const LEGS_CELL: [f32; 4] = [0.46875, 0.6875, 0.171875, 0.3203125];
|
|
const WEAPON_CELL: [f32; 4] = [0.0, 0.140625, 0.3203125, 0.6640625];
|
|
const SHIELD_CELL: [f32; 4] = [0.1875, 0.375, 0.3203125, 0.5546875];
|
|
|
|
const RED: [f32; 4] = [0.93, 0.07, 0.07, 1.0];
|
|
const YELLOW: [f32; 4] = [1.0, 0.82, 0.18, 1.0];
|
|
|
|
/// The show/hide + color law across the three states: clean (hidden), broken main hand (the
|
|
/// weapon glyph alone, red), damaged legs (ALL body pieces show, legs yellow, the rest faded
|
|
/// white), then repaired (hidden again).
|
|
#[test]
|
|
fn armor_guy_shows_red_broken_yellow_damaged_and_hides_clean() {
|
|
let _data = benilla_formats::wow_data_or_skip!();
|
|
let mut s = harness();
|
|
// The ref's initial settle: the frame is authored shown; PLAYER_ENTERING_WORLD's SetAlerts
|
|
// hides it while everything is clean.
|
|
s.fire_event("PLAYER_ENTERING_WORLD", vec![ScriptValue::Str("".into())]);
|
|
assert!(
|
|
!s.eval::<bool>("return DurabilityFrame:IsShown()").unwrap(),
|
|
"clean gear → no armor guy"
|
|
);
|
|
|
|
// Broken main hand: the weapon glyph alone, painted the ref's red.
|
|
let mut inv: InventorySlots = Default::default();
|
|
inv[16] = slot(25, Some((0, 20)));
|
|
s.set_inventory_slots(inv);
|
|
assert!(s.errors().is_empty(), "alert errors: {:?}", s.errors());
|
|
assert!(
|
|
s.eval::<bool>("return DurabilityFrame:IsShown() and DurabilityWeapon:IsShown()")
|
|
.unwrap(),
|
|
"a broken weapon shows the frame + its glyph"
|
|
);
|
|
assert!(
|
|
!s.eval::<bool>("return DurabilityHead:IsShown()").unwrap(),
|
|
"no body alert → the body stays hidden"
|
|
);
|
|
assert_eq!(cell_color(&mut s, WEAPON_CELL), Some(RED), "broken → red");
|
|
|
|
// Damaged legs (3 points left — the byte law's ABSOLUTE 1..=5, wow-re inventory-alert-law
|
|
// `cmp [D+0xa0],5`): every body piece shows, legs yellow, the un-alerted head the faded
|
|
// white(0.5); the weapon glyph (repaired now) hides.
|
|
let mut inv: InventorySlots = Default::default();
|
|
inv[7] = slot(39, Some((3, 25)));
|
|
s.set_inventory_slots(inv);
|
|
assert!(
|
|
s.eval::<bool>(
|
|
"return DurabilityFrame:IsShown() and DurabilityLegs:IsShown() and DurabilityHead:IsShown()"
|
|
)
|
|
.unwrap(),
|
|
"one body alert shows the whole body"
|
|
);
|
|
assert!(
|
|
!s.eval::<bool>("return DurabilityWeapon:IsShown()").unwrap(),
|
|
"the repaired weapon glyph hides"
|
|
);
|
|
assert_eq!(
|
|
cell_color(&mut s, LEGS_CELL),
|
|
Some(YELLOW),
|
|
"damaged → yellow"
|
|
);
|
|
assert_eq!(
|
|
cell_color(&mut s, HEAD_CELL),
|
|
Some([1.0, 1.0, 1.0, 0.5]),
|
|
"un-alerted body piece rides faded white"
|
|
);
|
|
|
|
// The threshold is ABSOLUTE, not a ratio: 5 points on a 100-max piece (5%) is damaged;
|
|
// 6 points on a 20-max piece (30%... and even 6/100) is not — `1..=5` exactly.
|
|
let mut inv: InventorySlots = Default::default();
|
|
inv[7] = slot(39, Some((5, 100)));
|
|
s.set_inventory_slots(inv);
|
|
assert!(
|
|
s.eval::<bool>("return DurabilityFrame:IsShown()").unwrap(),
|
|
"5 points left is damaged at ANY max (absolute law)"
|
|
);
|
|
let mut inv: InventorySlots = Default::default();
|
|
inv[7] = slot(39, Some((6, 20)));
|
|
s.set_inventory_slots(inv);
|
|
assert!(
|
|
!s.eval::<bool>("return DurabilityFrame:IsShown()").unwrap(),
|
|
"6 points left is never damaged (absolute law, no percentage)"
|
|
);
|
|
assert!(s.errors().is_empty(), "errors: {:?}", s.errors());
|
|
}
|
|
|
|
/// The FLAGS bits (wow-re inventory-alert-law): `0x10` forces red regardless of durability;
|
|
/// `0x08` (wrapped gift) silences the region entirely. And the client's 12th region — low ammo
|
|
/// (carried count <= 20 → 3) — answers through `GetInventoryAlertStatus(12)` even though the
|
|
/// 1.12 FrameXML never reads it.
|
|
#[test]
|
|
fn flag_bits_and_the_low_ammo_region() {
|
|
let _data = benilla_formats::wow_data_or_skip!();
|
|
let mut s = harness();
|
|
s.fire_event("PLAYER_ENTERING_WORLD", vec![ScriptValue::Str("".into())]);
|
|
|
|
// Force-red: full durability, bit 0x10 → status 4, the glyph paints red.
|
|
let mut inv: InventorySlots = Default::default();
|
|
let mut v = slot(25, Some((20, 20))).unwrap();
|
|
v.flags = 0x10;
|
|
inv[16] = Some(v);
|
|
s.set_inventory_slots(inv);
|
|
assert_eq!(
|
|
s.eval::<i64>("return GetInventoryAlertStatus(9)").unwrap(),
|
|
4,
|
|
"force-red bit → broken at full durability"
|
|
);
|
|
assert_eq!(cell_color(&mut s, WEAPON_CELL), Some(RED));
|
|
|
|
// Wrapped: broken durability but bit 0x08 → silent.
|
|
let mut inv: InventorySlots = Default::default();
|
|
let mut v = slot(25, Some((0, 20))).unwrap();
|
|
v.flags = 0x08;
|
|
inv[16] = Some(v);
|
|
s.set_inventory_slots(inv);
|
|
assert!(
|
|
!s.eval::<bool>("return DurabilityFrame:IsShown()").unwrap(),
|
|
"a wrapped item never alerts"
|
|
);
|
|
|
|
// Low ammo: 15 carried → region 12 reads 3; the armor guy stays hidden (FrameXML's 1..=11).
|
|
let mut inv: InventorySlots = Default::default();
|
|
inv[0] = slot(2512, None).map(|mut v| {
|
|
v.count = 15;
|
|
v
|
|
});
|
|
s.set_inventory_slots(inv);
|
|
assert_eq!(
|
|
s.eval::<i64>("return GetInventoryAlertStatus(12)").unwrap(),
|
|
3,
|
|
"low carried ammo reads 3 on the 12th region"
|
|
);
|
|
assert!(
|
|
!s.eval::<bool>("return DurabilityFrame:IsShown()").unwrap(),
|
|
"the 1.12 armor guy never shows for ammo"
|
|
);
|
|
assert!(s.errors().is_empty(), "errors: {:?}", s.errors());
|
|
}
|
|
|
|
/// The off-hand switch (ref SetAlerts' Shield arm): a broken SHIELD lights the shield glyph; a
|
|
/// broken off-hand WEAPON (template class 2) swaps it for the off-weapon glyph.
|
|
#[test]
|
|
fn off_hand_glyph_follows_what_the_hand_holds() {
|
|
let _data = benilla_formats::wow_data_or_skip!();
|
|
let mut s = harness();
|
|
s.set_item_template(
|
|
2362,
|
|
ItemTemplateView {
|
|
name: "Worn Wooden Shield".into(),
|
|
class: 4, // armor
|
|
..Default::default()
|
|
},
|
|
);
|
|
s.set_item_template(
|
|
2488,
|
|
ItemTemplateView {
|
|
name: "Worn Dagger".into(),
|
|
class: 2, // weapon
|
|
..Default::default()
|
|
},
|
|
);
|
|
|
|
// Broken shield → the shield glyph, red; the off-weapon glyph stays hidden.
|
|
let mut inv: InventorySlots = Default::default();
|
|
inv[17] = slot(2362, Some((0, 20)));
|
|
s.set_inventory_slots(inv);
|
|
assert!(
|
|
s.eval::<bool>("return DurabilityShield:IsShown() and not DurabilityOffWeapon:IsShown()")
|
|
.unwrap(),
|
|
"a shield lights the shield glyph"
|
|
);
|
|
assert_eq!(cell_color(&mut s, SHIELD_CELL), Some(RED));
|
|
|
|
// Broken off-hand weapon → the glyphs swap.
|
|
let mut inv: InventorySlots = Default::default();
|
|
inv[17] = slot(2488, Some((0, 16)));
|
|
s.set_inventory_slots(inv);
|
|
assert!(
|
|
s.eval::<bool>("return DurabilityOffWeapon:IsShown() and not DurabilityShield:IsShown()")
|
|
.unwrap(),
|
|
"an off-hand weapon swaps to the off-weapon glyph"
|
|
);
|
|
assert_eq!(
|
|
cell_color(&mut s, WEAPON_CELL),
|
|
Some(RED),
|
|
"the weapon cell paints the off-weapon red"
|
|
);
|
|
assert!(s.errors().is_empty(), "errors: {:?}", s.errors());
|
|
}
|
|
|
|
/// The manage pass owns the seat (ref UIParent.lua:1758-1768): the authored XML anchor (+40,
|
|
/// off-screen — director-caught) dies on the first pass; the frame's TOPRIGHT re-seats at the
|
|
/// cluster's BOTTOMRIGHT minus CONTAINER_OFFSET_X (0 with no right multibars), minus 20 more
|
|
/// while a side glyph (weapon/shield/ranged) extends the art rightward. OnShow/OnHide re-fire
|
|
/// the pass on every alert transition.
|
|
#[test]
|
|
fn manage_pass_seats_the_frame_inside_the_cluster_edge() {
|
|
let _data = benilla_formats::wow_data_or_skip!();
|
|
let mut s = harness();
|
|
s.fire_event("PLAYER_ENTERING_WORLD", vec![ScriptValue::Str("".into())]);
|
|
|
|
// Broken RANGED: the show transition's OnShow runs the pass; the RIGHT-side glyph pulls
|
|
// the frame 20 further left. (The ref's offset list deliberately excludes the main-hand
|
|
// weapon glyph — it hangs off the LEFT of the body and needs no extra room.)
|
|
let mut inv: InventorySlots = Default::default();
|
|
inv[18] = slot(2504, Some((0, 20)));
|
|
s.set_inventory_slots(inv);
|
|
s.resolve();
|
|
let delta: f32 = s
|
|
.eval("return MinimapCluster:GetRight() - DurabilityFrame:GetRight()")
|
|
.unwrap();
|
|
assert_eq!(
|
|
delta, 20.0,
|
|
"a right-side glyph seats the frame 20 further left"
|
|
);
|
|
|
|
// Repair (hide), then damaged legs (show; body only): flush with the cluster edge.
|
|
s.set_inventory_slots(Default::default());
|
|
let mut inv: InventorySlots = Default::default();
|
|
inv[7] = slot(39, Some((3, 25)));
|
|
s.set_inventory_slots(inv);
|
|
s.resolve();
|
|
let delta: f32 = s
|
|
.eval("return MinimapCluster:GetRight() - DurabilityFrame:GetRight()")
|
|
.unwrap();
|
|
assert_eq!(
|
|
delta, 0.0,
|
|
"body-only alerts seat flush with the cluster edge"
|
|
);
|
|
assert!(s.errors().is_empty(), "errors: {:?}", s.errors());
|
|
}
|
|
|
|
/// The seat stays fresh when a side glyph arrives AFTER the frame is already shown — the real
|
|
/// login order, equipment streaming in slot by slot with the weapon breaking before the
|
|
/// off-hand's slot lands (char "One": Worn Shortsword 0/20, then Large Round Shield 3/35). The
|
|
/// weapon glyph hangs off the body's LEFT and needs no room; once the shield glyph appears on the
|
|
/// RIGHT the frame must pull 20 further in so its ~17-unit overhang clears the screen edge.
|
|
/// Regression: the offset only refreshed on the frame's own show/hide transition, so a glyph that
|
|
/// appeared while the frame stayed shown left it stale and hung the shield off-screen
|
|
/// (director-caught). The OnEvent re-manage keeps it fresh on every alert recompute.
|
|
#[test]
|
|
fn a_late_side_glyph_refreshes_the_seat_while_the_frame_stays_shown() {
|
|
let _data = benilla_formats::wow_data_or_skip!();
|
|
let mut s = harness();
|
|
s.set_item_template(
|
|
25,
|
|
ItemTemplateView {
|
|
name: "Worn Shortsword".into(),
|
|
class: 2, // weapon
|
|
..Default::default()
|
|
},
|
|
);
|
|
s.set_item_template(
|
|
30,
|
|
ItemTemplateView {
|
|
name: "Large Round Shield".into(),
|
|
class: 4, // armor (a shield, not an off-hand weapon)
|
|
..Default::default()
|
|
},
|
|
);
|
|
s.fire_event("PLAYER_ENTERING_WORLD", vec![ScriptValue::Str("".into())]);
|
|
|
|
// 1) The weapon breaks first: the frame shows with only the LEFT-side weapon glyph → flush.
|
|
let mut inv: InventorySlots = Default::default();
|
|
inv[16] = slot(25, Some((0, 20)));
|
|
s.set_inventory_slots(inv);
|
|
s.resolve();
|
|
assert!(
|
|
s.eval::<bool>("return DurabilityFrame:IsShown() and not DurabilityShield:IsShown()")
|
|
.unwrap(),
|
|
"weapon-only: frame shown, no shield glyph yet"
|
|
);
|
|
let delta: f32 = s
|
|
.eval("return MinimapCluster:GetRight() - DurabilityFrame:GetRight()")
|
|
.unwrap();
|
|
assert_eq!(delta, 0.0, "the left-side weapon glyph needs no extra room");
|
|
|
|
// 2) The off-hand's slot lands later — the frame is ALREADY shown, so no show transition
|
|
// fires. The shield glyph now extends the art right; the seat must still pull in by 20.
|
|
let mut inv: InventorySlots = Default::default();
|
|
inv[16] = slot(25, Some((0, 20)));
|
|
inv[17] = slot(30, Some((3, 35)));
|
|
s.set_inventory_slots(inv);
|
|
s.resolve();
|
|
assert!(
|
|
s.eval::<bool>("return DurabilityShield:IsShown()").unwrap(),
|
|
"the off-hand's arrival shows the shield glyph"
|
|
);
|
|
let delta: f32 = s
|
|
.eval("return MinimapCluster:GetRight() - DurabilityFrame:GetRight()")
|
|
.unwrap();
|
|
assert_eq!(
|
|
delta, 20.0,
|
|
"a side glyph arriving while shown must still pull the frame 20 in"
|
|
);
|
|
// The director's symptom, directly: the shield glyph's right edge clears the screen.
|
|
let overhang: f32 = s
|
|
.eval("return DurabilityShield:GetRight() - GetScreenWidth()")
|
|
.unwrap();
|
|
assert!(
|
|
overhang <= 0.5,
|
|
"shield glyph clips off the right edge (overhang {overhang} px)"
|
|
);
|
|
assert!(s.errors().is_empty(), "errors: {:?}", s.errors());
|
|
}
|
|
|
|
/// The quest tracker drops below the durability guy instead of overlapping his corner (ref
|
|
/// UIParent.lua:1770 — QuestWatchFrame is last in the right-side walk, seated at the running
|
|
/// anchorY after the durability height is subtracted). Both frames anchor the same
|
|
/// MinimapCluster BOTTOMRIGHT; benilla shipped only the durability arm and left the tracker
|
|
/// colliding there (director-caught, over "The Captain's Chest").
|
|
#[test]
|
|
fn the_quest_tracker_stacks_below_the_durability_guy() {
|
|
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 [
|
|
"Interface\\FrameXML\\Fonts.xml",
|
|
"MoneyFrame.xml",
|
|
"Interface\\FrameXML\\GlobalStrings.lua",
|
|
"UiPanels.xml",
|
|
r"Interface\FrameXML\UIPanelTemplates.lua",
|
|
r"Interface\FrameXML\UIPanelTemplates.xml",
|
|
"Interface\\FrameXML\\BasicControls.xml",
|
|
"Interface\\FrameXML\\LocaleProperties.lua",
|
|
"Interface\\FrameXML\\StaticPopup.xml",
|
|
"UIParent.xml",
|
|
"GameTooltip.xml",
|
|
"MinimapCluster.xml",
|
|
"ScrollTemplates.xml",
|
|
"Interface\\FrameXML\\DurabilityFrame.xml",
|
|
"Interface\\FrameXML\\BasicControls.xml",
|
|
"Interface\\FrameXML\\ItemButtonTemplate.xml",
|
|
"Interface\\FrameXML\\QuestFrame.xml",
|
|
"MicroMenu.xml",
|
|
"Interface\\FrameXML\\QuestLogFrame.xml",
|
|
] {
|
|
load_xml(&s, f);
|
|
}
|
|
super::manifest::install_durability_reseat(&s).unwrap();
|
|
|
|
// A right-side glyph shows the 65-tall durability frame under the minimap.
|
|
let mut inv: InventorySlots = Default::default();
|
|
inv[18] = slot(2504, Some((0, 20)));
|
|
s.set_inventory_slots(inv);
|
|
s.resolve();
|
|
|
|
let dur_bottom: f32 = s.eval("return DurabilityFrame:GetBottom()").unwrap();
|
|
let watch_top: f32 = s.eval("return QuestWatchFrame:GetTop()").unwrap();
|
|
assert!(
|
|
(watch_top - dur_bottom).abs() <= 0.5,
|
|
"tracker top {watch_top} must sit flush under the durability bottom {dur_bottom}"
|
|
);
|
|
assert!(s.errors().is_empty(), "errors: {:?}", s.errors());
|
|
}
|