Files
benilla-octo/crates/benilla-app/src/ui_script/micro_menu_tests.rs
T
SamandClaude Fable 5.1 c3555b4eea the cooldown goes stock, early-Z comes back, and lists reach the end
- 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>
2026-09-06 19:35:31 +07:00

451 lines
18 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The stock `Interface\FrameXML\MainMenuBarMicroButtons.xml` — the eight micro buttons in the
//! main bar's right-hand recess — off the player's chain behind the stock bar (their anchor
//! target `MainMenuBarArtFrame`) into a bare engine (decision 1987).
//!
//! What these guard, in order: the row's geometry (29×58 at art-frame BOTTOMLEFT +(552,2), a
//! 26 px stride, and the reference's own 1 px nudge once the talent gate has run); the
//! `HitRectInsets` header that keeps a button's transparent top from eating the mouse; the
//! character button's portrait crop; `UpdateTalentButton`'s level-10 gate closing the row up
//! behind the hidden talent button; the two-line hover with the bound key in its label; and,
//! under the whole shipped manifest, `UpdateMicroButtons` following a real panel and the
//! level-up pulse the reference hangs on the talent button.
use benilla_ui::script::{QuadContent, ScriptValue, TexCoords, UiScript, UnitState};
/// The eight, in row order.
const ROW: [&str; 8] = [
"CharacterMicroButton",
"SpellbookMicroButton",
"TalentMicroButton",
"QuestLogMicroButton",
"SocialsMicroButton",
"WorldMapMicroButton",
"MainMenuMicroButton",
"HelpMicroButton",
];
/// The stock bar, then the stock row, into a 1024×768 engine, asserting both load clean. 1024
/// wide is deliberate: the 1024-wide bar then centers at x=0, so every reference offset below is
/// also an absolute screen coordinate.
fn harness() -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
for file in [
"Interface\\FrameXML\\Fonts.xml",
"Interface\\FrameXML\\Cooldown.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
r"Interface\FrameXML\UIParent.xml",
// The eight labels and their NEWBIE_TOOLTIP_* lines, read at each button's OnLoad…
"Interface\\FrameXML\\GlobalStrings.lua",
// …through `TEXT()`.
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\MainMenuBar.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
r"Interface\FrameXML\MainMenuBarMicroButtons.xml",
] {
super::test_ui::load_ui(&s, file);
}
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
s
}
/// Set the player's level and fire the `UNIT_LEVEL` the talent gate listens for.
fn set_player_level(s: &mut UiScript, level: u32) {
s.set_unit(
"player",
Some(UnitState {
exists: true,
level,
..Default::default()
}),
);
s.fire_event("UNIT_LEVEL", vec![ScriptValue::Str("player".into())]);
}
fn left_of(s: &UiScript, name: &str) -> f64 {
s.eval::<f64>(&format!("return {name}:GetLeft()"))
.unwrap_or_else(|e| panic!("{name}: {e}"))
}
/// The row's geometry, the reference's own numbers: CharacterMicroButton at the art frame's
/// BOTTOMLEFT +(552,2), each button 29×58, the rest chained BOTTOMLEFT-to-previous-BOTTOMRIGHT
/// +(-3,0) ⇒ a 26 px stride, so the eight run x 552..757 as declared — between the page arrows
/// (x≈522) and the bag cluster at the far right. **As declared** is the operative phrase: the XML
/// seats the quest-log button at −3 and `UpdateTalentButton` re-seats it at −2
/// (`MainMenuBarMicroButtons.lua:139`), so the first level pass at 10+ nudges the tail right by
/// one pixel. A real row is 1 px wider past its first `PLAYER_ENTERING_WORLD` than its XML
/// says; that is the client's, and it is asserted here as the reference's law rather than
/// sanded off (our retired file wrote −3 both ways).
#[test]
fn the_micro_row_sits_where_the_reference_puts_it() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.resolve();
// As declared, before any level pass: a clean 26 px stride.
for (i, name) in ROW.iter().enumerate() {
let (left, bottom, w, h) = s
.eval::<(f64, f64, f64, f64)>(&format!(
"return {name}:GetLeft(), {name}:GetBottom(), {name}:GetWidth(), {name}:GetHeight()"
))
.unwrap_or_else(|e| panic!("{name}: {e}"));
assert_eq!((w, h), (29.0, 58.0), "{name} size");
assert_eq!(
left,
552.0 + 26.0 * i as f64,
"{name} left edge, as declared"
);
assert_eq!(bottom, 2.0, "{name} sits 2 above the bar's bottom");
}
// Past level 10 the gate re-seats the quest-log button at −2: the tail moves 1 px right.
set_player_level(&mut s, 60);
s.resolve();
for (i, name) in ROW.iter().enumerate().skip(3) {
assert_eq!(
left_of(&s, name),
553.0 + 26.0 * i as f64,
"{name} after the talent gate's own −2"
);
}
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// The `HitRectInsets top="18"` header: the micro-button art fills only the lower ~40 of the 58,
/// and the empty top must stay transparent to the mouse — otherwise the row eats hover over the
/// bar's XP strip and the sliver of world above it.
#[test]
fn the_transparent_top_of_a_micro_button_does_not_capture_the_mouse() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.resolve();
// Mid-button horizontally (552 + 14), inside the art band (y 2..42) ⇒ captures.
assert_eq!(
s.hit_test_name(566.0, 20.0).as_deref(),
Some("CharacterMicroButton"),
"the art band takes the mouse"
);
// Same column, in the dead 18-unit header (y 42..60) ⇒ nothing of the row captures it.
assert_ne!(
s.hit_test_name(566.0, 50.0).as_deref(),
Some("CharacterMicroButton"),
"the inset header must be transparent to the mouse"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// The character button's face: `MicroButtonPortrait` binds the SAME `"player"` portrait slot
/// the unit frame samples, and carries the reference's crop window out to the renderer. Both
/// halves matter — the crop is what makes an 18×25 rectangle show a face instead of the whole
/// square bake squashed into it. Pushing the button swaps the window and dims it
/// (`CharacterMicroButton_SetPushed`, `MainMenuBarMicroButtons.lua:109-117`).
#[test]
fn the_character_button_carries_the_player_portrait_through_the_reference_crop() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.resolve();
let window = |s: &mut UiScript| {
s.extract()
.into_iter()
.find_map(|q| match q.content {
QuadContent::Texture {
portrait_unit: Some(unit),
tex_coords,
circular,
..
} => Some((unit, tex_coords, circular)),
_ => None,
})
.expect("the micro button's portrait quad")
};
let (unit, coords, circular) = window(&mut s);
assert_eq!(unit, "player");
assert!(
circular,
"the reference's round stencil lives in the bake's own UV space, so the crop below yields \
a rectangular slice OF a masked face — not an ellipse fitted to this 18x25 region"
);
let round4 = |c: TexCoords| match c {
TexCoords::Rect(e) => e.map(|v| (v * 10_000.0).round() / 10_000.0),
TexCoords::Corners(_) => panic!("the 4-edge form"),
};
assert_eq!(
round4(coords.expect("a crop window")),
[0.2, 0.8, 0.0666, 0.9],
"the normal window (CharacterMicroButton_SetNormal)"
);
s.run("CharacterMicroButton_SetPushed()").unwrap();
s.resolve();
let (_, coords, _) = window(&mut s);
assert_eq!(
round4(coords.expect("a crop window")),
[0.2666, 0.8666, 0.0, 0.8333],
"the held-down window (CharacterMicroButton_SetPushed)"
);
assert_eq!(
s.eval::<f64>("return MicroButtonPortrait:GetAlpha()")
.unwrap(),
0.5,
"…and the face dims while the button is down"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// `UpdateTalentButton` (l.133-141): under level 10 the talent button is hidden and the quest
/// log button slides onto its seat, so the row has no hole in it; at 10 the button returns and
/// the tail shifts back out — by one stride less two, the gate's own −2.
#[test]
fn the_talent_button_appears_at_level_ten_and_the_row_closes_up_below_it() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
set_player_level(&mut s, 9);
s.resolve();
assert!(
!s.eval::<bool>("return TalentMicroButton:IsVisible()")
.unwrap(),
"no talents before 10"
);
// The quest log button takes the talent button's own seat — slot 3, not slot 4.
assert_eq!(left_of(&s, "QuestLogMicroButton"), 552.0 + 26.0 * 2.0);
assert_eq!(
left_of(&s, "HelpMicroButton"),
552.0 + 26.0 * 6.0,
"the whole tail moves up one slot with it"
);
set_player_level(&mut s, 10);
s.resolve();
assert!(
s.eval::<bool>("return TalentMicroButton:IsVisible()")
.unwrap(),
"the button returns at 10"
);
assert_eq!(
left_of(&s, "QuestLogMicroButton"),
553.0 + 26.0 * 3.0,
"…and the tail goes back out, at the gate's own −2"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// The hover (the template's OnEnter, `MainMenuBarMicroButtons.xml:12-14`): every button's plate
/// is the reference's TWO-line `GameTooltip_AddNewbieTip` — the label, then that button's own
/// `NEWBIE_TOOLTIP_*` explanation in gold. 1.12 ships detailed tips ON (`SHOW_NEWBIE_TIPS = "1"`,
/// `UIOptionsFrame.lua:100`; ours sits in OptionsFrame.xml's uvar block), so the paragraph is the
/// DEFAULT hover (decision 0661). Through the engine's own hover, because the stock OnEnter reads
/// the firing frame off `this`. For the bound ones the label carries the key the way the
/// reference prints it — `GetBindingKey`'s raw token in `NORMAL_FONT_COLOR_CODE` parentheses
/// (`MicroButtonTooltipText`, l.11-18) — re-read on `UPDATE_BINDINGS`, which is the event
/// `bindings.rs` fires after every (re)load of the table.
#[test]
fn every_micro_button_hovers_with_its_reference_explanation() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.run("SHOW_NEWBIE_TIPS = \"1\"").unwrap();
// A key the character button's label can carry: the registry, then the reference's own
// `SetBinding`, then the event the row re-reads its labels on.
s.register_bindings(&crate::bindings::registry_commands());
assert_eq!(
s.eval::<Option<i64>>("return SetBinding(\"C\", \"TOGGLECHARACTER0\")")
.unwrap(),
Some(1)
);
s.fire_event("UPDATE_BINDINGS", vec![]);
s.resolve();
for (button, label, newbie) in [
(
"CharacterMicroButton",
"CHARACTER_BUTTON",
"NEWBIE_TOOLTIP_CHARACTER",
),
// The spellbook's own OnEnter (l.79-87) picks its label by `PlayerHasSpells()`.
(
"SpellbookMicroButton",
"PlayerHasSpells() and SPELLBOOK_ABILITIES_BUTTON or ABILITYBOOK_BUTTON",
"NEWBIE_TOOLTIP_SPELLBOOK",
),
(
"TalentMicroButton",
"TALENTS_BUTTON",
"NEWBIE_TOOLTIP_TALENTS",
),
(
"QuestLogMicroButton",
"QUESTLOG_BUTTON",
"NEWBIE_TOOLTIP_QUESTLOG",
),
(
"SocialsMicroButton",
"SOCIAL_BUTTON",
"NEWBIE_TOOLTIP_SOCIAL",
),
(
"WorldMapMicroButton",
"WORLDMAP_BUTTON",
"NEWBIE_TOOLTIP_WORLDMAP",
),
(
"MainMenuMicroButton",
"MAINMENU_BUTTON",
"NEWBIE_TOOLTIP_MAINMENU",
),
("HelpMicroButton", "HELP_BUTTON", "NEWBIE_TOOLTIP_HELP"),
] {
super::test_ui::hover(&mut s, button);
assert_eq!(
s.eval::<i64>("return GameTooltip:NumLines()").unwrap(),
2,
"{button}: the label, then the explanation"
);
let label = s.eval::<String>(&format!("return {label}")).unwrap();
let line1 = s
.eval::<String>("return GameTooltipTextLeft1:GetText()")
.unwrap();
assert!(
line1.starts_with(&label),
"{button}: line 1 is {line1:?}, expected it to open with {label:?}"
);
assert_eq!(
s.eval::<String>("return GameTooltipTextLeft2:GetText()")
.unwrap(),
s.eval::<String>(&format!("return {newbie}")).unwrap(),
"{button}: line 2 is the reference's {newbie}, verbatim"
);
assert_eq!(
s.eval::<i64>("return GameTooltip.default").unwrap(),
1,
"{button}: the default-corner anchor, not ANCHOR_RIGHT off the button"
);
super::test_ui::unhover(&mut s);
}
// The bound one carries its key the reference's way: the raw token, gold, in parentheses,
// after a space — `MicroButtonTooltipText`'s own concatenation.
super::test_ui::hover(&mut s, "CharacterMicroButton");
let line1 = s
.eval::<String>("return GameTooltipTextLeft1:GetText()")
.unwrap();
assert_eq!(
line1,
s.eval::<String>(
"return CHARACTER_BUTTON .. \" \" .. NORMAL_FONT_COLOR_CODE .. \"(C)\" .. FONT_COLOR_CODE_CLOSE"
)
.unwrap(),
"the key read on UPDATE_BINDINGS"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// Under the whole shipped manifest — the only VM in which every panel `UpdateMicroButtons`
/// (l.20-84) reads exists — a button is PUSHED exactly while its panel is up, the character
/// button's face dims with it, and the level-up pulse the reference hangs on the talent button
/// (`TalentMicroButton_OnEvent`, l.119-124: sixty seconds, unless the character sheet is open)
/// is live — 0304's "no microbutton pulse" scope-out, retired by the file that owns the pulse.
#[test]
fn a_micro_button_pushes_while_its_panel_is_open_and_the_talent_button_pulses_on_a_ding() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
// A player always exists by the time the manifest loads (1051/1848) — with a race and a
// class, which the sheet's own `PaperDollFrame_SetLevel` formats into its title on show.
s.set_unit(
"player",
Some(UnitState {
exists: true,
name: Some("Probefour".into()),
level: 60,
race: Some("Night Elf".into()),
race_file: Some("NightElf".into()),
class: Some("Warrior".into()),
class_file: Some("WARRIOR".into()),
sex: 2,
is_player: true,
player_controlled: true,
..Default::default()
}),
);
// The reference's strings, which the app runs ahead of the manifest (lifecycle.rs) — the
// ding's chat lines below format `SPELL_STAT0_NAME`..`4` through them.
super::test_ui::load_ui(&s, "Interface\\FrameXML\\GlobalStrings.lua");
assert!(super::load_default_ui(&s).is_empty());
s.resolve();
let state = |s: &UiScript, button: &str| {
s.eval::<String>(&format!("return {button}:GetButtonState()"))
.unwrap()
};
let face = |s: &UiScript| {
s.eval::<f64>("return MicroButtonPortrait:GetAlpha()")
.unwrap()
};
assert_eq!(state(&s, "CharacterMicroButton"), "NORMAL");
assert_eq!(face(&s), 1.0);
// The button's own click (its OnClick, l.49-51) — the sheet's OnShow calls back.
s.run("ToggleCharacter(\"PaperDollFrame\")").unwrap();
assert_eq!(
state(&s, "CharacterMicroButton"),
"PUSHED",
"open sheet ⇒ button held down"
);
assert_eq!(
face(&s),
0.5,
"…and the face dims (CharacterMicroButton_SetPushed)"
);
s.run("ToggleCharacter(\"PaperDollFrame\")").unwrap();
assert_eq!(state(&s, "CharacterMicroButton"), "NORMAL");
assert_eq!(face(&s), 1.0);
// The reference's own full-screen panel; its OnShow/OnHide call back the same way.
s.run("ToggleWorldMap()").unwrap();
assert_eq!(state(&s, "WorldMapMicroButton"), "PUSHED");
s.run("ToggleWorldMap()").unwrap();
assert_eq!(state(&s, "WorldMapMicroButton"), "NORMAL");
// A ding with the sheet closed: sixty seconds on the pulse list (UiPanels.xml's
// SetButtonPulse, the reference's UIParent.lua machinery). The nine args are the ones
// `ui_unit` fires (level, health, power, talent points, five stats).
s.fire_event(
"PLAYER_LEVEL_UP",
vec![
ScriptValue::Int(61),
ScriptValue::Int(12),
ScriptValue::Int(0),
ScriptValue::Int(1),
ScriptValue::Int(1),
ScriptValue::Int(1),
ScriptValue::Int(1),
ScriptValue::Int(0),
ScriptValue::Int(0),
],
);
assert_eq!(
s.eval::<f64>("return TalentMicroButton.pulseTimeLeft")
.unwrap(),
60.0,
"the sixty-second pulse"
);
assert!(
s.eval::<bool>(
"for _, b in ipairs(PULSEBUTTONS) do if b == TalentMicroButton then return true end end \
return false"
)
.unwrap(),
"on the pulse list"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}