- 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>
357 lines
16 KiB
Rust
357 lines
16 KiB
Rust
//! Drives the REAL `Interface\FrameXML\PaperDollFrame.xml` stat rows through the engine — the
|
||
//! first test that executes the paper doll's own `PaperDollFrame_Set*` Lua at all, and since
|
||
//! decision 1751 that Lua is the reference's own rather than a transcription of it.
|
||
//!
|
||
//! That gap is why this file exists. The character sheet's whole buff-decomposition surface — the
|
||
//! green number, the red number, the `(base +x -y)` tooltip — sat broken from the day it shipped
|
||
//! (B165, B251) because the four `POSSTAT`/`NEGSTAT`/`RESISTANCEBUFFMODS*` descriptor arrays were
|
||
//! decoded as f32 when the wire carries INT, so every real buff arrived as a rounded `0` and every
|
||
//! row took its "nothing is modifying this" leg. Both gates were green throughout: the bindings'
|
||
//! own unit tests fed the snapshot directly, and nothing ever ran the Lua that colours the row.
|
||
//!
|
||
//! So the assertions here are deliberately at the *rendered string*: what `CharacterStatFrameNStatText`
|
||
//! and `MagicResTextN` actually carry after a repaint, colour escapes included. Three shapes per
|
||
//! family — untouched (plain), buffed (green), debuffed (red) — plus the tooltip's base arithmetic,
|
||
//! which is the guard for `UnitStat`'s first return being the RAW field rather than a pre-subtracted
|
||
//! one (decision 1397: the ref Lua subtracts `posBuff`/`negBuff` itself, so a pre-subtracted first
|
||
//! return deducts the buff twice — a defect the f32 bug was hiding).
|
||
|
||
mod common;
|
||
|
||
use benilla_ui::script::{UiScript, UnitCombatStats, UnitState};
|
||
|
||
/// The paper doll's load prefix, in `assets/ui/benilla.toc` order.
|
||
///
|
||
/// **`CharacterFrame.xml` is deliberately not here.** These tests repaint the stat rows directly;
|
||
/// they never open the window, and `PaperDollFrame`'s only tie to its container is a `parent=`
|
||
/// name, which the loader warns about and falls back from. Pulling the container in would drag
|
||
/// `CharacterFrame_OnLoad`'s four external dependencies (the unit frames, the XP bar,
|
||
/// `TextStatusBar.lua`, the panel-tab kit — see `ui_script::test_ui::CHARACTER_UI`) into a test
|
||
/// about arithmetic.
|
||
///
|
||
/// `GlobalStrings.lua` and `BasicControls.xml` are not scenery either: the stock file has no
|
||
/// `X = X or "…"` fallbacks, and every row here formats a real string — `SPELL_STAT0_NAME`..`4`
|
||
/// through `TEXT()` (`PaperDollFrame.lua:143`), `RESISTANCE<n>_NAME` and
|
||
/// `RESISTANCE_TOOLTIP_SUBTEXT` (`:184`/`:224`), `ARMOR` and `ARMOR_TOOLTIP` (`:236`/`:249`).
|
||
const FILES: &[&str] = &[
|
||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||
"Interface\\FrameXML\\Fonts.xml",
|
||
"Interface\\FrameXML\\BasicControls.xml",
|
||
// The reference's own since 1751 window 24 — `common::load_ui` speaks both stores.
|
||
"Interface\\FrameXML\\ItemButtonTemplate.xml",
|
||
r"Interface\FrameXML\MoneyFrame.lua",
|
||
r"Interface\FrameXML\MoneyFrame.xml",
|
||
// `Model_OnLoad` — the model pane's `<OnLoad>` calls it, so this is a LOAD-time dependency of
|
||
// the paper doll, not of the window. The reference declares it in `UIParent.lua`; ours lives
|
||
// in our counterpart of that file.
|
||
r"Interface\FrameXML\UIParent.xml",
|
||
r"Interface\FrameXML\UIPanelTemplates.lua",
|
||
r"Interface\FrameXML\UIPanelTemplates.xml",
|
||
"Interface\\FrameXML\\LocaleProperties.lua",
|
||
"Interface\\FrameXML\\StaticPopup.xml", // the dialog engine (1960)
|
||
"Interface\\FrameXML\\GameTooltip.xml",
|
||
// `CooldownFrameTemplate` and `CooldownFrame_SetTimer`. Not scenery: every one of the 20 slot
|
||
// buttons runs `PaperDollItemSlotButton_Update` from its own OnLoad, which calls
|
||
// `CooldownFrame_SetTimer` on `$parentCooldown` unconditionally (`PaperDollFrame.lua:692`) —
|
||
// so leaving this out is 20 loader errors before the first assertion.
|
||
"Interface\\FrameXML\\Cooldown.xml",
|
||
"Interface\\FrameXML\\PaperDollFrame.xml",
|
||
];
|
||
|
||
fn load_ui(script: &UiScript) {
|
||
for file in FILES {
|
||
common::load_ui(script, file);
|
||
}
|
||
}
|
||
|
||
/// The two colour escapes the sheet writes, as `Fonts.xml` defines them — asserted against the
|
||
/// rendered string rather than re-derived, so a palette change fails here loudly instead of
|
||
/// silently agreeing with itself.
|
||
const GREEN: &str = "|cff20ff20";
|
||
const RED: &str = "|cffff2020";
|
||
|
||
/// A level-60 body carrying one of each case: strength heavily geared (+105), agility cursed
|
||
/// (−12), stamina untouched, and the same three shapes across the resistance schools. The negative
|
||
/// halves are the wire words an **x86**-hosted server sends; an arm64 host saturates a debuff to a
|
||
/// flat `0` on the way out (decision 1397), which is exactly why the red leg has to be exercised
|
||
/// here and cannot be exercised against the local deploy.
|
||
fn stats() -> UnitCombatStats {
|
||
UnitCombatStats {
|
||
stats: [225, 68, 178, 34, 51],
|
||
stat_pos: [105, 0, 0, 4, 0],
|
||
stat_neg: [0, -12, 0, 0, 0],
|
||
// schools: [0] armor, then holy/fire/nature/frost/shadow/arcane.
|
||
resistances: [2965, 0, 65, 20, 0, 0, 0],
|
||
resistance_pos: [0, 0, 65, 0, 0, 0, 0],
|
||
resistance_neg: [0, 0, 0, -10, 0, 0, 0],
|
||
..Default::default()
|
||
}
|
||
}
|
||
|
||
fn player() -> UnitState {
|
||
UnitState {
|
||
exists: true,
|
||
name: Some("Probefour".into()),
|
||
level: 60,
|
||
race: Some("Human".into()),
|
||
class: Some("Warrior".into()),
|
||
..Default::default()
|
||
}
|
||
}
|
||
|
||
/// Repaint the sheet the way an in-world `UNIT_STATS` does and read a row's rendered text back.
|
||
fn painted(script: &mut UiScript) {
|
||
script
|
||
.run("PaperDollFrame_SetStats() PaperDollFrame_SetResistances() PaperDollFrame_SetArmor()")
|
||
.expect("the paper doll's stat rows repaint");
|
||
}
|
||
|
||
fn text_of(script: &mut UiScript, region: &str) -> String {
|
||
script
|
||
.eval::<String>(&format!(r#"return getglobal("{region}"):GetText()"#))
|
||
.unwrap_or_else(|e| panic!("reading {region}: {e}"))
|
||
}
|
||
|
||
fn tooltip_of_field(script: &mut UiScript, frame: &str, field: &str) -> String {
|
||
script
|
||
.eval::<String>(&format!(r#"return getglobal("{frame}").{field}"#))
|
||
.unwrap_or_else(|e| panic!("reading {frame}.{field}: {e}"))
|
||
}
|
||
|
||
fn tooltip_of(script: &mut UiScript, frame: &str) -> String {
|
||
script
|
||
.eval::<String>(&format!(r#"return getglobal("{frame}").tooltip"#))
|
||
.unwrap_or_else(|e| panic!("reading {frame}.tooltip: {e}"))
|
||
}
|
||
|
||
fn seated() -> UiScript {
|
||
let mut script = UiScript::new().expect("a UI VM");
|
||
load_ui(&script);
|
||
script.set_unit("player", Some(player()));
|
||
script.set_player_combat_stats(Some(stats()));
|
||
script
|
||
}
|
||
|
||
#[test]
|
||
fn a_geared_stat_renders_green_and_its_tooltip_names_the_unbuffed_base() {
|
||
let _data = benilla_formats::wow_data_or_skip!();
|
||
let mut s = seated();
|
||
painted(&mut s);
|
||
|
||
// Strength: 225 total off a 120 base, +105 from gear. Green number, and the tooltip's
|
||
// parenthesised base is the ONE subtraction the ref Lua performs (225 − 105 − 0 = 120).
|
||
let text = text_of(&mut s, "CharacterStatFrame1StatText");
|
||
assert!(
|
||
text.starts_with(GREEN) && text.contains("225"),
|
||
"a gear-boosted strength must render green, got {text:?}"
|
||
);
|
||
let tip = tooltip_of(&mut s, "CharacterStatFrame1");
|
||
assert!(
|
||
tip.contains("Strength 225") && tip.contains("(120") && tip.contains("+105"),
|
||
"the tooltip must read the total, the unbuffed base and the delta, got {tip:?}"
|
||
);
|
||
assert!(
|
||
!tip.contains("(15"),
|
||
"base 15 = 225 − 105 − 105: the buff was deducted twice, so UnitStat's first return is \
|
||
pre-subtracted. It must be the raw UNIT_FIELD_STAT — {tip:?}"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn a_debuffed_stat_renders_red_and_an_untouched_one_stays_plain() {
|
||
let _data = benilla_formats::wow_data_or_skip!();
|
||
let mut s = seated();
|
||
painted(&mut s);
|
||
|
||
let agility = text_of(&mut s, "CharacterStatFrame2StatText");
|
||
assert!(
|
||
agility.starts_with(RED) && agility.contains("68"),
|
||
"a cursed agility must render red, got {agility:?}"
|
||
);
|
||
// Stamina has neither half — the ref's `posBuff == 0 and negBuff == 0` leg: a bare number with
|
||
// no escape at all. This is the assertion the shipped bug would have PASSED, and it is here so
|
||
// a future over-correction that colours everything is caught.
|
||
let stamina = text_of(&mut s, "CharacterStatFrame3StatText");
|
||
assert_eq!(
|
||
stamina, "178",
|
||
"an unmodified stat carries no colour escape at all"
|
||
);
|
||
// Red wins over green when both halves are in play — the ref colours on `negBuff < 0` first.
|
||
s.set_player_combat_stats(Some(UnitCombatStats {
|
||
stat_pos: [105, 30, 0, 4, 0],
|
||
..stats()
|
||
}));
|
||
painted(&mut s);
|
||
let mixed = text_of(&mut s, "CharacterStatFrame2StatText");
|
||
assert!(
|
||
mixed.starts_with(RED),
|
||
"a stat with both a buff and a debuff renders RED, got {mixed:?}"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn resistance_rows_colour_by_which_half_is_bigger() {
|
||
let _data = benilla_formats::wow_data_or_skip!();
|
||
let mut s = seated();
|
||
painted(&mut s);
|
||
|
||
// Resolve each row's school from the FRAME's own `id=`, never from a table: that is exactly
|
||
// what the reference's `PaperDollFrame_SetResistances` does (`UnitResistance("player",
|
||
// frame:GetID())`), and it means a row-order change cannot silently re-point these assertions.
|
||
// It used to read our deleted file's `BENILLA_MAGICRES_IDS`; the stock XML carries the same
|
||
// mapping on the frames themselves (`MagicResFrame1 id="6"` arcane, then 2..5 fire/nature/
|
||
// frost/shadow — `PaperDollFrame.xml:654-758`).
|
||
let row_of = |s: &mut UiScript, school: i64| -> usize {
|
||
s.eval::<i64>(&format!(
|
||
"for i = 1, NUM_RESISTANCE_TYPES do \
|
||
if getglobal(\"MagicResFrame\"..i):GetID() == {school} then return i end \
|
||
end return 0"
|
||
))
|
||
.expect("the school→row map") as usize
|
||
};
|
||
|
||
let fire = row_of(&mut s, 2);
|
||
let nature = row_of(&mut s, 3);
|
||
assert!(fire > 0 && nature > 0, "fire and nature both have rows");
|
||
|
||
let fire_text = text_of(&mut s, &format!("MagicResText{fire}"));
|
||
assert!(
|
||
fire_text.starts_with(GREEN) && fire_text.contains("65"),
|
||
"a +65 fire resistance renders green, got {fire_text:?}"
|
||
);
|
||
let nature_text = text_of(&mut s, &format!("MagicResText{nature}"));
|
||
assert!(
|
||
nature_text.starts_with(RED),
|
||
"a nature school whose debuff outweighs its buff renders red, got {nature_text:?}"
|
||
);
|
||
}
|
||
|
||
/// **The melee block: the seven-value `UnitDamage` unpacked positionally by the reference's own
|
||
/// Lua**, and the arithmetic it does with the last three.
|
||
///
|
||
/// The stat rows above pin `UnitStat`/`UnitResistance`/`UnitArmor`. This one pins the family
|
||
/// decision 1793 warns about hardest, because its consumer is arithmetic rather than a lookup:
|
||
/// `PaperDollFrame_SetDamage` destructures **seven** values and immediately divides by the
|
||
/// seventh. One value short and `percent` is nil — `(minDamage / nil)` raises; one value long and
|
||
/// every term after it is off by a slot and the numbers come out plausible and wrong. Asserting
|
||
/// the rendered strings is what tells those two apart from correct.
|
||
///
|
||
/// The numbers below are the reference's own formula, not ours: `displayMin`/`displayMax` are the
|
||
/// raw range floored/ceiled, while the TOOLTIP range is the same numbers with the multiplier
|
||
/// divided out and the flat bonuses subtracted (`PaperDollFrame.lua:279-283`) — which is why
|
||
/// "40 - 62" and "27 - 47" are both right and different.
|
||
#[test]
|
||
fn the_melee_block_unpacks_seven_values_and_does_the_references_arithmetic() {
|
||
let _data = benilla_formats::wow_data_or_skip!();
|
||
let mut s = seated();
|
||
// 40.5-61.5 at 2.6 s, +12/-3 flat and a x1.1 multiplier: every one of the seven slots is
|
||
// distinct and non-zero, so a slot read from the wrong position cannot coincide with the
|
||
// right answer.
|
||
s.set_player_combat_stats(Some(UnitCombatStats {
|
||
main_attack_time_ms: 2600,
|
||
min_damage: 40.5,
|
||
max_damage: 61.5,
|
||
physical_bonus_pos: 12,
|
||
physical_bonus_neg: -3,
|
||
damage_percent: 1.1,
|
||
attack_power: 780,
|
||
attack_power_pos: 30,
|
||
attack_power_neg: -10,
|
||
..stats()
|
||
}));
|
||
s.run("PaperDollFrame_SetDamage() PaperDollFrame_SetAttackPower()")
|
||
.expect("the melee rows repaint");
|
||
|
||
// The ROW: the raw range, green because the bonuses are net positive.
|
||
assert_eq!(
|
||
text_of(&mut s, "CharacterDamageFrameStatText"),
|
||
format!("{GREEN}40 - 62|r")
|
||
);
|
||
// The TOOLTIP: the same range with the multiplier divided out and the flat bonuses removed,
|
||
// then each modifier appended in the reference's own order and colours.
|
||
assert_eq!(
|
||
tooltip_of_field(&mut s, "CharacterDamageFrame", "damage"),
|
||
format!("27 - 47{GREEN} +12|r{RED} -3|r{GREEN} x110%|r")
|
||
);
|
||
let speed: f64 = s
|
||
.eval("return CharacterDamageFrame.attackSpeed")
|
||
.expect("the hover's attack speed");
|
||
assert!((speed - 2.6).abs() < 1e-6, "got {speed}");
|
||
// dps = fullDamage / speed, fullDamage = (base + pos + neg) * percent = 51.0.
|
||
let dps: f64 = s.eval("return CharacterDamageFrame.dps").expect("the dps");
|
||
assert!((dps - 51.0 / 2.6).abs() < 0.01, "got {dps}");
|
||
// No offhand weapon: the reference NILS the offhand speed, which is what its hover branches on.
|
||
assert!(s
|
||
.eval::<Option<f64>>("return CharacterDamageFrame.offhandAttackSpeed")
|
||
.unwrap()
|
||
.is_none());
|
||
|
||
// Attack power goes through `PaperDollFormatStat`, whose colour law is "red if anything is
|
||
// negative, green otherwise" — so a mixed pair is RED even though the net is positive.
|
||
assert_eq!(
|
||
text_of(&mut s, "CharacterAttackPowerFrameStatText"),
|
||
format!("{RED}800|r")
|
||
);
|
||
let ap_tip = tooltip_of(&mut s, "CharacterAttackPowerFrame");
|
||
assert!(
|
||
ap_tip.contains("800") && ap_tip.contains("(780") && ap_tip.contains("+30"),
|
||
"the AP tooltip names the effective, the base and the buff, got {ap_tip:?}"
|
||
);
|
||
|
||
// The plain leg: no bonuses and no multiplier, so `totalBonus == 0` and the row carries no
|
||
// colour escape at all — the branch a wrong `percent` would silently take.
|
||
s.set_player_combat_stats(Some(UnitCombatStats {
|
||
main_attack_time_ms: 2600,
|
||
min_damage: 40.5,
|
||
max_damage: 61.5,
|
||
damage_percent: 1.0,
|
||
..stats()
|
||
}));
|
||
s.run("PaperDollFrame_SetDamage()").unwrap();
|
||
assert_eq!(text_of(&mut s, "CharacterDamageFrameStatText"), "40 - 62");
|
||
}
|
||
|
||
/// **The ranged block with nothing in the ranged slot** — the `NOT_APPLICABLE` fallback all three
|
||
/// ranged rows share, and the `PaperDollFrame.noRanged` latch that carries it between them.
|
||
///
|
||
/// It is the reachable state for eight of nine classes at level 1, and it is the branch that
|
||
/// decides whether `PaperDollFrame_SetRangedDamage` unpacks `UnitRangedDamage`'s six values at
|
||
/// all — so it has to be pinned before the numbers are worth anything.
|
||
#[test]
|
||
fn the_ranged_rows_fall_back_to_not_applicable_with_an_empty_ranged_slot() {
|
||
let _data = benilla_formats::wow_data_or_skip!();
|
||
let mut s = seated();
|
||
s.run(
|
||
"PaperDollFrame_SetRangedAttack() PaperDollFrame_SetRangedDamage() PaperDollFrame_SetRangedAttackPower()",
|
||
)
|
||
.expect("the ranged rows repaint");
|
||
|
||
let na: String = s
|
||
.eval("return NOT_APPLICABLE")
|
||
.expect("the ref's own string");
|
||
for row in [
|
||
"CharacterRangedAttackFrameStatText",
|
||
"CharacterRangedDamageFrameStatText",
|
||
"CharacterRangedAttackPowerFrameStatText",
|
||
] {
|
||
assert_eq!(text_of(&mut s, row), na, "{row}");
|
||
}
|
||
// …and the hover has nothing to show, which is the guard
|
||
// `CharacterRangedDamageFrame_OnEnter` opens with.
|
||
assert!(s
|
||
.eval::<Option<String>>("return CharacterRangedDamageFrame.damage")
|
||
.unwrap()
|
||
.is_none());
|
||
}
|
||
|
||
/// Armor is the one row of this family the local server never decomposes — item armor lands in
|
||
/// `UNIT_FIELD_RESISTANCES[0]` itself, not in the buff split — so its plain-number leg is the live
|
||
/// shape and worth pinning as such.
|
||
#[test]
|
||
fn armor_with_no_buff_split_renders_plain() {
|
||
let _data = benilla_formats::wow_data_or_skip!();
|
||
let mut s = seated();
|
||
painted(&mut s);
|
||
assert_eq!(text_of(&mut s, "CharacterArmorFrameStatText"), "2965");
|
||
}
|