Files
benilla-octo/crates/benilla-app/src/ui_script/minimap_tests.rs
T
gromandClaude Opus 5 7fef604615 portrait: a unit pane honours Model:SetPosition
`SetPosition` is the widget's model-root translate and the extract spent it only on the file-pane
path. The booth path dropped the value and posed `R(yaw) · S(s)` alone, so Tmog's wheel zoom, its
right-drag pan and the per-slot framing of every browse card moved nothing at all.

It shows on a `<PlayerModel>` and not on a plain `<Model>` for one reason: this widget's camera is
frozen (`0x7acf10`) where `<Model>`'s perspective leg republishes its authored camera through the
very root being moved (`0x718960`), which cancels. `capture::probe_model_camera`'s `position` leg
pins that half; this is the other.

The extract publishes the translate in bevy's axes into a new `PanePositions`, keyed by slot so the
four stock window panes and the stock dressing room are served by the same channel rather than a
second path; a slot that is not drawn keeps the translation it has rather than snapping back to the
origin. The booths apply it as one `RootPose { yaw, scale, pos }`, latched together — keyed on
`(yaw, scale)` alone a position-only change compares equal and writes nothing, so a wheel would
zoom once and then stop. `WOW_BOOTH_LOG` gains `pane pose slot=… yaw=… scale=… pos=[…]`, one line
per change.

One model unit per `SetPosition` unit, and deliberately not the reference's `pos · layoutScale`:
that factor belongs to the group the reference's root scales with it, and this booth's root carries
only the renormalizer (`framing::pane_root_scale_at` drops `modelScale` and `layoutScale` both), so
taking the translate's share alone would move a `scale="4"` pane's model four times its own size.
The magnitude is the one part nothing in the tree pins — the only two calibrated `SetPosition`
sites are the ortho leg's pixel ladder and the `<Model>` leg where it cancels — and the extract's
arm says what to revisit if a wheel notch turns out to zoom too hard.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-16 18:45:25 -06:00

613 lines
24 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 `Minimap.xml` driven engine-only. Indoor and outdoor zoom are independent levels, so
//! the +/- buttons must re-sync when a WMO transition switches between them.
use benilla_ui::script::UiScript;
use super::test_ui::load_ui as load_xml;
fn enabled(s: &UiScript, button: &str) -> bool {
s.eval::<bool>(&format!("return {button}:IsEnabled() ~= 0"))
.unwrap()
}
#[test]
fn minimap_zoom_buttons_resync_when_switching_inside_and_outside() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
// Host globals the cluster calls that a bare engine does not install.
s.run("function GetMinimapZoneText() return '' end")
.unwrap();
s.run("function PlaySound() end").unwrap();
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
// `GameTooltip` before the cluster, as shipped: `Minimap_Update` touches it from `OnLoad` on.
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\BattlefieldFrame.xml");
load_xml(&s, "Interface\\FrameXML\\Minimap.xml");
// The zoom CVar defaults to 3, a middle level, so both buttons start enabled.
assert_eq!(s.eval::<u8>("return Minimap:GetZoom()").unwrap(), 3);
assert!(enabled(&s, "MinimapZoomIn"));
assert!(enabled(&s, "MinimapZoomOut"));
// Outdoors at 5, the top level: `MINIMAP_UPDATE_ZOOM` greys ZoomIn (`Minimap.lua:56-64`).
s.run("Minimap:SetZoom(5)").unwrap();
s.fire_event("MINIMAP_UPDATE_ZOOM", vec![]);
assert!(!enabled(&s, "MinimapZoomIn"), "at max zoom ZoomIn disables");
assert!(enabled(&s, "MinimapZoomOut"));
// Indoors reads the indoor level, still 3; the app fires `MINIMAP_UPDATE_ZOOM` on the switch.
s.set_minimap_inside(true);
s.fire_event("MINIMAP_UPDATE_ZOOM", vec![]);
assert_eq!(
s.eval::<u8>("return Minimap:GetZoom()").unwrap(),
3,
"indoors reads the separate indoor index"
);
assert!(
enabled(&s, "MinimapZoomIn"),
"the stale outdoor max-zoom greying must clear — this is the reported bug"
);
assert!(enabled(&s, "MinimapZoomOut"));
s.run("Minimap:SetZoom(0)").unwrap();
s.fire_event("MINIMAP_UPDATE_ZOOM", vec![]);
assert!(
!enabled(&s, "MinimapZoomOut"),
"at min zoom ZoomOut disables"
);
assert!(enabled(&s, "MinimapZoomIn"));
s.set_minimap_inside(false);
s.fire_event("MINIMAP_UPDATE_ZOOM", vec![]);
assert_eq!(s.eval::<u8>("return Minimap:GetZoom()").unwrap(), 5);
assert!(!enabled(&s, "MinimapZoomIn"), "outdoor max-zoom survived");
assert!(enabled(&s, "MinimapZoomOut"));
}
/// Stock `MiniMapTrackingFrame` (`Minimap.xml:109-174`) follows `GetTrackingTexture()` on
/// `PLAYER_AURAS_CHANGED`, which the aura feed fires beside `UNIT_AURA`.
#[test]
fn tracking_frame_follows_get_tracking_texture_across_player_auras_changed() {
let _data = benilla_formats::wow_data_or_skip!();
use benilla_ui::script::TrackingState;
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
s.run("function GetMinimapZoneText() return '' end")
.unwrap();
s.run("function PlaySound() end").unwrap();
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\BattlefieldFrame.xml");
load_xml(&s, "Interface\\FrameXML\\Minimap.xml");
let vis = |s: &UiScript| {
s.eval::<bool>("return MiniMapTrackingFrame:IsVisible()")
.unwrap()
};
assert!(!vis(&s), "no tracking at load — the frame starts hidden");
// A tracking aura: Find Minerals, spell 2580.
s.set_tracking(Some(TrackingState {
spell_id: 2580,
name: Some("Find Minerals".into()),
icon: Some("Interface\\Icons\\Trade_Mining".into()),
cancelable: true,
}));
s.fire_event("PLAYER_AURAS_CHANGED", vec![]);
assert!(vis(&s), "a live tracking texture shows the icon");
s.set_tracking(None);
s.fire_event("PLAYER_AURAS_CHANGED", vec![]);
assert!(!vis(&s), "no tracking texture hides the frame again");
}
/// The cluster and the stock `GameTime.xml`, the clock at `hour:minute` as the app pushes it.
fn game_time_session(hour: u32, minute: u32) -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
s.run("function GetMinimapZoneText() return '' end")
.unwrap();
s.run("function PlaySound() end").unwrap();
s.run(&format!(
"__benilla_game_hour = {hour}; __benilla_game_minute = {minute}"
))
.unwrap();
// `GameTime.lua` reads `TwentyFourHourTime`, which `LocalizeFrames` sets; the reference calls
// that on `VARIABLES_LOADED` (`UIParent.lua:231-232`), and this session has no UIParent.
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "Interface\\FrameXML\\Localization.xml");
s.run("LocalizeFrames()").unwrap();
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
// `TEXT()`, which the tooltip formatting goes through.
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\BattlefieldFrame.xml");
load_xml(&s, "Interface\\FrameXML\\Minimap.xml");
load_xml(&s, "Interface\\FrameXML\\GameTime.xml");
s
}
/// `GameTimeTexture`'s texcoords as `(left, right, top, bottom)`: `GetTexCoord` answers eight
/// values, `ULx, ULy, LLx, LLy, URx, URy, LRx, LRy`.
fn tod_window(s: &UiScript) -> (f64, f64, f64, f64) {
let (ulx, uly, _, lly, urx, ..): (f64, f64, f64, f64, f64, f64, f64, f64) =
s.eval("return GameTimeTexture:GetTexCoord()").unwrap();
(ulx, urx, uly, lly)
}
/// Stock `GameTimeFrame_Update` slides the 50px window +0.5 onto the moon when
/// `time < DAWN or time >= DUSK`, 5:30 and 21:00 (`GameTime.lua:2-18`).
#[test]
fn game_time_frame_slides_the_sun_moon_window_on_the_game_clock() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = game_time_session(10, 30);
// `OnLoad` seeds `timeOfDay = 0`, so its own update already seats the window.
let day = (0.0, 50.0 / 128.0, 0.0, 50.0 / 64.0);
assert_eq!(tod_window(&s), day, "mid-morning shows the sun half");
s.run("__benilla_game_hour = 21; __benilla_game_minute = 0")
.unwrap();
s.tick(0.016);
assert_eq!(
tod_window(&s),
(0.5, 0.5 + 50.0 / 128.0, 0.0, 50.0 / 64.0),
"9:00 PM sharp is the moon half"
);
s.run("__benilla_game_hour = 5; __benilla_game_minute = 29")
.unwrap();
s.tick(0.016);
assert_eq!(tod_window(&s).0, 0.5, "5:29 AM is still the moon");
s.run("__benilla_game_minute = 30").unwrap();
s.tick(0.016);
assert_eq!(tod_window(&s), day, "5:30 AM sharp flips to the sun");
}
/// `GameTimeFrame` declares no `enableMouse`: its `<Scripts>` enable the mouse, and its
/// `<HitRectInsets>` trim the hit rect (`GameTime.xml:15-17`).
#[test]
fn hovering_the_indicator_shows_and_live_updates_the_game_time_tooltip() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = game_time_session(21, 7);
s.resolve();
// The middle of the hit rect, inset by l=6, r=0, t=5, b=10.
let (l, r, t, b) = (
s.eval::<f32>("return GameTimeFrame:GetLeft()").unwrap(),
s.eval::<f32>("return GameTimeFrame:GetRight()").unwrap(),
s.eval::<f32>("return GameTimeFrame:GetTop()").unwrap(),
s.eval::<f32>("return GameTimeFrame:GetBottom()").unwrap(),
);
let (x, y) = ((l + 6.0 + r) * 0.5, (b + 10.0 + t - 5.0) * 0.5);
s.mouse_move(x, y);
assert!(
s.eval::<bool>("return GameTooltip:IsVisible()").unwrap(),
"OnEnter owns the tooltip (the Scripts-walker auto-enable capturing at {x},{y})"
);
// The enGB `LocalizeFrames` sets `TwentyFourHourTime = 1` (`Localization.lua:20`): 21:07.
let text = |s: &UiScript| {
s.eval::<String>("return GameTooltipTextLeft1:GetText()")
.unwrap()
};
assert_eq!(text(&s), "21:07");
// While owned, `GameTimeFrame_Update` refreshes the tooltip (`GameTime.lua:20-22`).
s.run("__benilla_game_minute = 8").unwrap();
s.tick(0.016);
assert_eq!(text(&s), "21:08", "the owned tooltip follows the clock");
// Inside the raw 50x50 rect but in the left inset: not hoverable.
s.mouse_move(l + 2.0, y);
assert!(
!s.eval::<bool>("return GameTooltip:IsVisible()").unwrap(),
"the 6-px left inset band is not hoverable"
);
}
/// A real click reaches `Minimap_OnClick` via `OnMouseUp` and parks centre-relative UI units.
#[test]
fn a_click_on_the_minimap_parks_a_centre_relative_ping_request() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
s.run("function GetMinimapZoneText() return '' end")
.unwrap();
s.run("function PlaySound() end").unwrap();
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\BattlefieldFrame.xml");
load_xml(&s, "Interface\\FrameXML\\Minimap.xml");
s.resolve();
let cx = s
.eval::<f32>("local x = Minimap:GetCenter(); return x")
.unwrap();
let cy = s
.eval::<f32>("local _, y = Minimap:GetCenter(); return y")
.unwrap();
assert!(cx > 0.0 && cy > 0.0, "the widget resolved: ({cx}, {cy})");
assert_eq!(s.take_minimap_ping_request(), None);
// 20 right and 12 up, inside the 70-unit disc; stock `Minimap_OnClick` first adds
// `CURSOR_OFFSET_X/Y`, -7 and -9 (`Minimap.lua:3-4`, `:133-134`).
s.mouse_button(cx + 20.0, cy + 12.0, "LeftButton", true);
s.mouse_button(cx + 20.0, cy + 12.0, "LeftButton", false);
let (dx, dy) = s
.take_minimap_ping_request()
.expect("OnMouseUp → Minimap_OnClick → PingLocation");
assert!((dx - 13.0).abs() < 0.01, "x right of centre, less 7: {dx}");
assert!((dy - 3.0).abs() < 0.01, "y UP from centre, less 9: {dy}");
assert_eq!(s.take_minimap_ping_request(), None);
s.mouse_button(10.0, 10.0, "LeftButton", true);
s.mouse_button(10.0, 10.0, "LeftButton", false);
assert_eq!(s.take_minimap_ping_request(), None, "off-widget is no ping");
}
/// The reference answers from statics nothing clears (`0x4eefd0`), and stock `Minimap_OnUpdate`
/// uses the pair with no nil test; here it is the app's last publish, `(0, 0)` before any.
#[test]
fn get_ping_position_answers_two_numbers_always() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
s.run("function GetMinimapZoneText() return '' end")
.unwrap();
s.run("function PlaySound() end").unwrap();
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\BattlefieldFrame.xml");
load_xml(&s, "Interface\\FrameXML\\Minimap.xml");
assert_eq!(s.arity("Minimap:GetPingPosition()").unwrap(), 2);
assert_eq!(
s.eval::<f32>("return (Minimap:GetPingPosition())").unwrap(),
0.0,
"before any ping: a number, not nil"
);
s.set_minimap_ping((0.25, -0.125));
let x = s
.eval::<f32>("local x = Minimap:GetPingPosition(); return x")
.unwrap();
let y = s
.eval::<f32>("local _, y = Minimap:GetPingPosition(); return y")
.unwrap();
assert!(
(x - 0.25).abs() < 1e-6 && (y + 0.125).abs() < 1e-6,
"{x} {y}"
);
// `MINIMAP_PING` shows the frame for 5 s, re-seated from `GetPingPosition` each frame, then
// fades over 0.5 s (`Minimap.lua:1-2`, `:68-82`).
assert!(
!s.eval::<bool>("return MiniMapPing:IsVisible()").unwrap(),
"hidden until a ping"
);
s.fire_event(
"MINIMAP_PING",
vec![
benilla_ui::script::ScriptValue::Str("player".into()),
benilla_ui::script::ScriptValue::Number(0.25),
benilla_ui::script::ScriptValue::Number(-0.125),
],
);
assert!(s.eval::<bool>("return MiniMapPing:IsVisible()").unwrap());
s.tick(1.0);
assert!(
s.eval::<bool>("return MiniMapPing:IsVisible()").unwrap(),
"held at 1 s"
);
let cx = s
.eval::<f32>(
"local x = MiniMapPing:GetCenter(); local mx = Minimap:GetCenter(); return x - mx",
)
.unwrap();
assert!(
(cx - 0.25 * 140.0).abs() < 0.5,
"re-seated at the normalized offset times the width: {cx}"
);
s.tick(4.2);
s.tick(0.6);
assert!(
!s.eval::<bool>("return MiniMapPing:IsVisible()").unwrap(),
"5 s hold + 0.5 s fade, then hidden"
);
}
/// Stock `MiniMapMeetingStoneFrame` follows `IsInMeetingStoneQueue()` on `MEETINGSTONE_CHANGED`.
#[test]
fn the_meeting_stone_icon_follows_the_queue_across_meetingstone_changed() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
s.run("function GetMinimapZoneText() return '' end")
.unwrap();
s.run("function PlaySound() end").unwrap();
for f in [
"Interface\\FrameXML\\Fonts.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\Localization.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\BasicControls.xml",
r"Interface\FrameXML\UIParent.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\StaticPopup.xml",
"Interface\\FrameXML\\BattlefieldFrame.xml",
"Interface\\FrameXML\\Minimap.xml",
] {
load_xml(&s, f);
}
s.resolve();
let vis = |s: &UiScript| {
s.eval::<bool>("return MiniMapMeetingStoneFrame:IsVisible()")
.unwrap()
};
assert!(!vis(&s), "hidden at load");
s.set_meeting_stone(1519, Some("Looking for more for Stormwind City".into()));
s.fire_event("MEETINGSTONE_CHANGED", vec![]);
assert!(vis(&s), "a queued area shows the icon");
s.run("this = MiniMapMeetingStoneFrame; MiniMapMeetingStoneFrame:GetScript('OnEnter')()")
.unwrap();
assert_eq!(
s.eval::<String>("return GameTooltipTextLeft1:GetText()")
.unwrap(),
"Looking for more for Stormwind City"
);
s.run("MiniMapMeetingStoneFrame:Click()").unwrap();
assert!(
s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap(),
"a click asks CONFIRM_LEAVE_QUEUE"
);
s.run("StaticPopup1Button1:Click()").unwrap();
assert_eq!(
s.take_meeting_stone_cancels(),
1,
"Accept is CancelMeetingStoneRequest()"
);
s.set_meeting_stone(0, Some("Looking for more for Unknown".into()));
s.fire_event("MEETINGSTONE_CHANGED", vec![]);
assert!(!vis(&s), "area 0 hides it again");
}
/// A shown ping pane publishes one tile request and no quad; once the renderer hands back a cell,
/// the cell draws every frame in the overlay lane, even when nothing in the interface moves.
#[test]
fn a_shown_ping_pane_asks_for_a_tile_and_draws_its_cell() {
use bevy::prelude::*;
use bevy::window::PrimaryWindow;
use benilla_ui::widget::{ModelFileFacts, SequenceFacts};
use crate::ui_models::{Cell, UiModelTiles};
use crate::ui_pass::UiQuads;
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
s.run("function GetMinimapZoneText() return '' end")
.unwrap();
s.run("function PlaySound() end").unwrap();
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\BattlefieldFrame.xml");
load_xml(&s, "Interface\\FrameXML\\Minimap.xml");
// The file's facts, as `benilla-extract m2seq` reads them; the `OnLoad`'s `SetSequence(0)`
// waited for them and replays now.
const PING: &str = r"Interface\MiniMap\Ping\MinimapPing.mdx";
let seq = |anim_id, duration_ms, looping| SequenceFacts {
anim_id,
duration_ms,
looping,
};
s.set_model_facts(
PING,
ModelFileFacts {
sequences: vec![seq(127, 1333, false), seq(0, 833, true), seq(1, 333, false)],
bbox: ([0.0; 3], [0.0; 3]),
cameras: 0,
},
);
assert!(
s.visible_model_panes().is_empty(),
"hidden until a ping: nothing to paint"
);
let ping_at = |s: &mut UiScript, nx: f64, ny: f64| {
s.fire_event(
"MINIMAP_PING",
vec![
benilla_ui::script::ScriptValue::Str("player".into()),
benilla_ui::script::ScriptValue::Number(nx),
benilla_ui::script::ScriptValue::Number(ny),
],
);
};
ping_at(&mut s, 0.25, -0.125);
let panes = s.visible_model_panes();
assert_eq!(panes.len(), 1, "the ping pane is on the paint list");
let pane = panes[0];
assert_eq!(
pane.play.map(|p| (p.anim_id, p.cursor_ms)),
Some((0, 0)),
"the OnLoad's SetSequence(0) replayed over the seed: Stand, at 0"
);
let mut app = App::new();
app.insert_non_send_resource(s);
app.init_resource::<UiQuads>();
// The pass's handover: the tick half writes it, the paint half reads it.
app.init_resource::<crate::ui_script::UiPassState>();
app.init_resource::<Assets<Image>>();
app.init_resource::<crate::portrait::PortraitImages>();
app.init_resource::<crate::portrait::BoothPanes>();
app.init_resource::<crate::portrait::PaneBakeSizes>();
app.init_resource::<crate::portrait::PanePositions>();
app.init_resource::<crate::portrait::UnitPaneBooths>();
app.init_resource::<UiModelTiles>();
app.init_resource::<crate::minimap::MinimapWidget>();
app.init_resource::<crate::ui_script::UiFrameCost>();
app.init_resource::<crate::ui_script::UiCostWanted>();
app.init_resource::<Time>();
app.init_resource::<Time<Real>>();
app.init_resource::<crate::ui_script::UiClock>();
app.init_resource::<crate::ui_script::UiScaleCvar>();
app.world_mut().spawn((
Window {
resolution: UVec2::new(1024, 768).into(),
..default()
},
PrimaryWindow,
));
// The app's order: extract, then compose after `sync_tiles`, whose cells this harness hands
// over by hand; the lane is cleared between frames, as `clear_ui_overlays` does.
app.add_systems(
Update,
(
(super::extract::tick_script, super::extract::paint_script).chain(),
crate::ui_models::compose_tiles,
)
.chain(),
);
app.update();
let premultiplied = |app: &App| {
let quads = app.world().resource::<UiQuads>();
assert!(
quads.quads.iter().all(|q| !q.premultiplied),
"the base lane never carries a tile: the composite is not the extract's"
);
quads
.overlays
.iter()
.filter(|q| q.premultiplied)
.cloned()
.collect::<Vec<_>>()
};
let next_frame = |app: &mut App| {
app.world_mut().resource_mut::<UiQuads>().overlays.clear();
app.update();
};
{
let tiles = app.world().resource::<UiModelTiles>();
let req = tiles
.requests
.get(&pane.handle)
.expect("one request for the ping pane");
assert_eq!(req.path, PING);
// `<Size>50×50</Size>` at s = 1 (768-tall window), DPI 1: 50 device px a side.
assert_eq!(req.size_px, UVec2::new(50, 50));
// `scale="0.4"`: 1280 * 0.4 = 512 px per model unit.
assert!(
(req.px_per_unit - 512.0).abs() < 1e-3,
"{}",
req.px_per_unit
);
// A layout unit, and a particle's unit: 768 · √((4/3)² + 1) = 1280 at 4:3.
assert!(
(req.pos_px_per_unit - 1280.0).abs() < 0.1,
"{}",
req.pos_px_per_unit
);
assert!(
(req.star_px_per_unit - 1280.0).abs() < 0.1,
"{}",
req.star_px_per_unit
);
assert_eq!(req.icon, None);
// The pane's rect in the quad pass's space (y-down window px), and its own alpha.
assert!(
(req.rect.width() - 50.0).abs() < 1e-3 && (req.rect.height() - 50.0).abs() < 1e-3,
"{:?}",
req.rect
);
assert!((req.alpha - 1.0).abs() < 1e-6);
assert!(premultiplied(&app).is_empty(), "no cell yet: nothing drawn");
}
// The renderer hands a cell back and nothing else changes; the next frame still draws it.
{
let atlas = app
.world_mut()
.resource_mut::<Assets<Image>>()
.add(Image::default());
let mut tiles = app.world_mut().resource_mut::<UiModelTiles>();
tiles.atlas = Some(atlas);
tiles.atlas_size = UVec2::splat(512);
tiles.cells.insert(
pane.handle,
Cell {
origin: UVec2::new(2, 2),
size: UVec2::new(50, 50),
},
);
}
next_frame(&mut app);
let drawn = premultiplied(&app);
assert_eq!(drawn.len(), 1, "the cell, once, on a quiet frame");
let q = &drawn[0];
assert!((q.rect.width() - 50.0).abs() < 1e-3 && (q.rect.height() - 50.0).abs() < 1e-3);
let [tl, _, br, _] = q.uv.corners;
assert!((tl[0] - 2.0 / 512.0).abs() < 1e-6 && (tl[1] - 2.0 / 512.0).abs() < 1e-6);
assert!((br[0] - 52.0 / 512.0).abs() < 1e-6 && (br[1] - 52.0 / 512.0).abs() < 1e-6);
assert_eq!(q.color, [1.0, 1.0, 1.0, 1.0], "the frame's own alpha");
assert!(q.texture.is_some());
// The lane is re-emitted every frame, not on change.
next_frame(&mut app);
assert_eq!(
premultiplied(&app).len(),
1,
"still drawn on the frame after"
);
{
let mut script = app.world_mut().non_send_resource_mut::<UiScript>();
// `Minimap_OnUpdate` re-seats the frame from `GetPingPosition()` (`Minimap.lua:74`).
script.set_minimap_ping((-0.125, 0.25));
ping_at(&mut script, -0.125, 0.25);
}
next_frame(&mut app);
let moved = premultiplied(&app);
assert_eq!(moved.len(), 1, "one quad after the pane moved");
assert_ne!(moved[0].rect, q.rect, "the composite followed the pane");
}