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>
This commit is contained in:
Sam
2026-09-06 19:35:31 +07:00
co-authored by Claude Fable 5.1
parent c92ca1b8ec
commit c3555b4eea
93 changed files with 2423 additions and 1643 deletions
+3 -57
View File
@@ -1,64 +1,10 @@
//! The Cooldown widget kind (decision 0137 phase 4): the `SetCooldown` API, the reference
//! machine's derived phases through extraction, and the tick auto-hide (`Cooldown.lua`'s
//! `OnAnimFinished` → `Hide` edge), plus the per-action dynamic-state read API.
//! The cooldown **bindings**: the per-action dynamic-state read API and the `GetTime`-space
//! cooldown triples the stock `Cooldown.lua` machine consumes (the widget itself is the
//! reference's own `<Model>` since decision 2019 — `tests::model_clock` runs that file verbatim).
use super::common::script;
use crate::script::*;
fn cooldown_content(s: &UiScript) -> Option<(f32, Option<f32>)> {
s.extract().into_iter().find_map(|q| match q.content {
QuadContent::Cooldown { fraction, flash } => Some((fraction, flash)),
_ => None,
})
}
/// The machine, end to end on the engine clock: hidden → sweep (fraction scrubs with GetTime) →
/// the 1 s finish flash → auto-hidden by tick.
#[test]
fn cooldown_widget_sweeps_flashes_and_hides() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
cd = CreateFrame("Cooldown", "TestCooldown")
cd:SetWidth(36); cd:SetHeight(36); cd:SetPoint("CENTER")
cd:Hide()
"#,
)
.unwrap();
s.resolve();
assert_eq!(cooldown_content(&s), None, "hidden — no slot emitted");
// Arm a 10 s cooldown that started 2.5 s ago (the reference's start-in-the-past shape from
// GetActionCooldown) and show — the sweep reads 25 %.
s.run("cd:SetCooldown(GetTime() - 2.5, 10); cd:Show()")
.unwrap();
s.resolve();
let (fraction, flash) = cooldown_content(&s).expect("shown — the slot is emitted");
assert!(
(fraction - 0.25).abs() < 1e-3,
"sweep at 25%, got {fraction}"
);
assert_eq!(flash, None, "no flash while the sweep runs");
// Advance past the sweep end into the flash's middle: fraction ≥ 1, flash ≈ 0.5.
s.tick(8.0); // now 0.5 s into the flash window
s.resolve();
let (fraction, flash) = cooldown_content(&s).expect("flash phase still emits");
assert!(fraction >= 1.0);
let f = flash.expect("the finish flash runs for 1 s after the sweep");
assert!((f - 0.5).abs() < 0.05, "flash midway, got {f}");
// Past the flash: tick hides the widget (the OnAnimFinished → Hide edge).
s.tick(1.0);
s.resolve();
assert_eq!(cooldown_content(&s), None, "finished — tick hid the widget");
assert!(
!s.eval::<bool>("return cd:IsVisible() == 1").unwrap(),
"the frame itself is hidden, not just skipped"
);
}
/// The per-action dynamic-state API: the 1/nil conventions, IsActionInRange's tri-state, and
/// GetActionCooldown's GetTime-space triple that goes cold at expiry.
#[test]
@@ -409,6 +409,73 @@ fn a_loop_completes_once_and_a_fileless_pane_paints_nothing() {
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// **The implicit rect** (decision 2015): a model pane that authored no size takes its file's
/// bounding-box extent in LAYOUT units — `768·√(a²+1)` FrameXML units per unit, `1280` at 4:3
/// — the moment the facts are known; it follows the screen's aspect; an authored size wins.
#[test]
fn a_size_less_pane_takes_its_files_rect_in_layout_units() {
let mut s = script();
s.set_screen_size(1024.0, 768.0);
// The map arrow's own box (`MinimapArrow.m2`, render law §2): 0.0262 × 0.0263 units.
let arrow = |s: &mut UiScript| {
s.set_model_facts(
r"Interface\Minimap\MinimapArrow.mdx",
ModelFileFacts {
sequences: vec![SequenceFacts {
anim_id: 0,
duration_ms: 3333,
looping: true,
}],
bbox: ([-0.0127, -0.0118, 0.0], [0.0135, 0.0145, 0.0]),
},
);
};
s.run(
r#"
a = CreateFrame("Model", "Sized", UIParent)
a:SetPoint("CENTER")
b = CreateFrame("Model", "Authored", UIParent)
b:SetPoint("CENTER") b:SetWidth(50) b:SetHeight(20)
a:SetModel("Interface\\Minimap\\MinimapArrow.mdx")
b:SetModel("Interface\\Minimap\\MinimapArrow.mdx")
"#,
)
.unwrap();
s.resolve();
assert_eq!(
s.eval::<f32>("return Sized:GetWidth()").unwrap(),
0.0,
"no facts yet: no rect"
);
arrow(&mut s);
s.resolve();
let (w, h): (f32, f32) = s
.eval("return Sized:GetWidth(), Sized:GetHeight()")
.unwrap();
assert!(
(w - 0.0262 * 1280.0).abs() < 0.05 && (h - 0.0263 * 1280.0).abs() < 0.05,
"{w}×{h}"
);
let (bw, bh): (f32, f32) = s
.eval("return Authored:GetWidth(), Authored:GetHeight()")
.unwrap();
assert_eq!((bw, bh), (50.0, 20.0), "an authored size is untouched");
assert!(pane(&s, "Sized").implicit_size && !pane(&s, "Authored").implicit_size);
// 16:9 — a layout unit is 768·√((16/9)²+1) = 1566.4 FrameXML units.
s.set_screen_size(1600.0, 900.0);
s.resolve();
let w: f32 = s.eval("return Sized:GetWidth()").unwrap();
assert!((w - 0.0262 * 1566.4).abs() < 0.1, "{w}");
// Authoring a size later ends the implicit rect for good.
s.run("Sized:SetWidth(10)").unwrap();
assert!(!pane(&s, "Sized").implicit_size);
s.set_screen_size(1024.0, 768.0);
s.resolve();
assert_eq!(s.eval::<f32>("return Sized:GetWidth()").unwrap(), 10.0);
}
/// `ReplaceIconTexture` is the type-14 texture override on the INSTANCE: stored over a file
/// (queued and replayed while it streams), dropped with no file, released by `SetModel` and
/// `ClearModel`.
@@ -1464,9 +1464,6 @@ fn a_frames_type_chain_matches_the_roster() {
"DressUpModel",
&["DressUpModel", "PlayerModel", "Model", "Frame", "Region"],
),
// **Our Era-shaped divergence reports what 1.12's cooldown IS**: a Model. 1.12.1 has no
// `Cooldown` type name at all (the roster is 23 and none is that).
("Cooldown", "Model", &["Model", "Frame", "Region"]),
];
for (kind, leaf, chain) in cases {
s.run(&format!(r#"TC = CreateFrame("{kind}")"#))
@@ -68,10 +68,12 @@ fn get_scroll_child_roundtrips_wrapper_and_name_and_clears_on_nil() {
}
/// The sign convention (verified against the design's own worked example): frame top 500, vertical
/// 40 ⇒ child top 540. Also covers the 0 / clamped-to-range cases and that `SetScrollChild(nil)`
/// restores the child's own authored anchor (never mutated — the override is a local map).
/// 40 ⇒ child top 540. Also covers 0, an offset PAST the range and one below zero — both stored and
/// applied verbatim, since the reference's `0x786db0` never reads the range (decision 2017) — and
/// that `SetScrollChild(nil)` restores the child's own authored anchor (never mutated — the
/// override is a local map).
#[test]
fn scroll_child_top_tracks_vertical_scroll_and_clamps_then_restores_on_clear() {
fn scroll_child_top_tracks_vertical_scroll_unclamped_then_restores_on_clear() {
let mut s = script();
s.set_screen_size(800.0, 600.0); // screen rect: bottom 0, left 0, top 600, right 800
@@ -112,18 +114,34 @@ fn scroll_child_top_tracks_vertical_scroll_and_clamps_then_restores_on_clear() {
"vertical=40 -> child top = frame top + 40 (a positive offset lifts the child)"
);
// range = child_h(600) - frame_h(200) = 400; asking for 9999 clamps to it.
s.run("SF:SetVerticalScroll(9999)").unwrap();
// range = child_h(600) - frame_h(200) = 400, and the offset is NOT clamped to it: 450 is
// stored and applied as 450. (Keeping the bar inside the range is FrameXML's job, through the
// Slider's [min, max] — the engine has no opinion.)
s.run("SF:SetVerticalScroll(450)").unwrap();
s.resolve();
assert_eq!(
s.eval::<f32>("return SF:GetVerticalScroll()").unwrap(),
400.0
450.0,
"stored verbatim, the range of 400 notwithstanding"
);
let quads = s.extract();
assert_eq!(
marker_rect(&quads, "marker:child").map(|r| r.top),
Some(900.0),
"clamped: frame top 500 + range 400"
Some(950.0),
"applied verbatim: frame top 500 + 450"
);
// Nor at zero.
s.run("SF:SetVerticalScroll(-30)").unwrap();
s.resolve();
assert_eq!(
s.eval::<f32>("return SF:GetVerticalScroll()").unwrap(),
-30.0
);
let quads = s.extract();
assert_eq!(
marker_rect(&quads, "marker:child").map(|r| r.top),
Some(470.0),
"a negative offset lowers the child: frame top 500 - 30"
);
// SetScrollChild(nil): the override stops being computed — the child's own authored anchor
@@ -138,6 +156,41 @@ fn scroll_child_top_tracks_vertical_scroll_and_clamps_then_restores_on_clear() {
);
}
/// The reference's `0x786db0` is gated on the offset actually CHANGING (a compare against the
/// stored value, nothing else): a `SetVerticalScroll` to the value already held re-lays nothing out
/// and fires no `OnVerticalScroll`. The same shape as the Slider's `SetValue` change-gate (decision
/// 0250) — the other half of what keeps the bar ↔ frame wiring from ringing — and a value past the
/// range is a change like any other.
#[test]
fn set_vertical_scroll_fires_on_vertical_scroll_only_on_a_change() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
Fires = {}
local frame = CreateFrame("ScrollFrame", "SF")
frame:SetPoint("TOPLEFT", 0, -100)
frame:SetSize(300, 200)
frame:SetScript("OnVerticalScroll", function() table.insert(Fires, arg1) end)
local child = CreateFrame("Frame", "Child")
child:SetSize(300, 600)
frame:SetScrollChild(child)
frame:SetVerticalScroll(0) -- already 0: nothing
frame:SetVerticalScroll(40) -- a change
frame:SetVerticalScroll(40) -- the same again: nothing
frame:SetVerticalScroll(700) -- past the range (400): a change, stored as given
frame:SetVerticalScroll(0) -- back
"#,
)
.unwrap();
assert_eq!(
s.eval::<String>("return table.concat(Fires, \",\")")
.unwrap(),
"40,700,0"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
#[test]
fn vertical_scroll_range_is_live_and_zero_when_unresolved_or_childless_or_shorter() {
let mut s = script();
@@ -235,8 +288,11 @@ fn vertical_scroll_range_is_local_units_on_a_scaled_frame() {
);
}
/// `OnVerticalScroll` carries the value AS STORED — past the range too (decision 2017: the
/// reference's `0x786db0` fires with `[+0x328]`, which it never clamps) — under the RF-0025
/// conventions, and `UpdateScrollChildRect` fires `OnScrollRangeChanged` with the live range.
#[test]
fn vertical_scroll_fires_clamped_and_update_rect_fires_range_changed() {
fn vertical_scroll_fires_as_given_and_update_rect_fires_range_changed() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
@@ -265,8 +321,8 @@ fn vertical_scroll_fires_clamped_and_update_rect_fires_range_changed() {
s.run("SF:SetVerticalScroll(9999)").unwrap();
assert_eq!(
s.eval::<f32>("return seen_v").unwrap(),
400.0,
"fires with the CLAMPED value"
9999.0,
"fires with the value as given — the range (400) is not consulted"
);
s.run("SF:UpdateScrollChildRect()").unwrap();
+90 -2
View File
@@ -14,6 +14,9 @@ fn push_catalog(s: &mut UiScript) {
// Cell 8801 = the child (0.5, 0.5) resolves to through Elwynn's OWN loc rect — a "Stormwind
// City" peer child. Clicking it from the Elwynn zone map drills into the city (the §3 path).
ek_grid[8801] = 2;
// Cell 9060 = the child (0.82, 0.82) resolves to through Elwynn's own rect — Elwynn itself,
// the case the zone-level hover must stay silent on (the displayed zone never names itself).
ek_grid[9060] = 1;
s.set_world_map_catalog(vec![
WorldMapContinentView {
name: "Kalimdor".into(),
@@ -52,9 +55,23 @@ fn push_catalog(s: &mut UiScript) {
// A clean synthetic loc rect inside the EK continent rect: w=2000, h=1500 → the
// continent-highlight formula lands on round-ish expected coords (asserted below).
loc_rect: (-8000.0, -10000.0, -400.0, -1900.0),
// Two overlays shaped like real Elwynn rows: Northshire reveals on explore bit
// 125, Goldshire on bit 124 (the real AreaTable exploreFlags).
// Three overlays. Two are shaped like real Elwynn rows: Northshire reveals
// on explore bit 125, Goldshire on bit 124 (the real AreaTable exploreFlags);
// their hit rects (top, left, bottom, right; px of 1002×668) sit apart so a
// point is in at most one. The first is a row whose first area slot resolves
// to nothing (bit 126; its rect overlaps Northshire's) — the zone-level hover
// must walk past it, not stop at it.
overlays: vec![
WorldMapOverlayView {
texture: "Interface\\WorldMap\\Elwynn\\STONECAIRNLAKE".into(),
width: 128,
height: 128,
offset_x: 450,
offset_y: 300,
explore_bits: vec![126],
hit_rect: (300, 450, 400, 560),
area_name: None,
},
WorldMapOverlayView {
texture: "Interface\\WorldMap\\Elwynn\\NORTHSHIREVALLEY".into(),
width: 200,
@@ -62,6 +79,8 @@ fn push_catalog(s: &mut UiScript) {
offset_x: 490,
offset_y: 200,
explore_bits: vec![125],
hit_rect: (200, 490, 376, 690),
area_name: Some("Northshire Valley".into()),
},
WorldMapOverlayView {
texture: "Interface\\WorldMap\\Elwynn\\GOLDSHIRE".into(),
@@ -70,6 +89,8 @@ fn push_catalog(s: &mut UiScript) {
offset_x: 380,
offset_y: 320,
explore_bits: vec![124],
hit_rect: (400, 300, 520, 460),
area_name: Some("Goldshire".into()),
},
],
},
@@ -292,6 +313,73 @@ fn worldmap_click_containment_and_zone_grid() {
assert_eq!(s.eval::<i64>("return GetCurrentMapContinent()").unwrap(), 0);
}
/// At ZONE level `UpdateMapHighlight` answers a NAME only (report B360; wow-re 15b2a8ea §1a/1b/1d):
/// a revealed overlay's sub-area when the cursor is inside its hit rect, else the neighbouring
/// zone or city whose grid cell the cursor is in through the DISPLAYED zone's rect window — never
/// the displayed zone itself — and always a nil fileName + six zeros, so the stock frame hides the
/// highlight quad. Fogged sub-areas have no name; an overlay with no resolvable first area is
/// walked past. The reference's own Duskwood shot: "Deadwind Pass" in the label, no highlight.
#[test]
fn worldmap_zone_level_hover_names_without_highlight() {
let mut s = script();
push_catalog(&mut s);
s.run("SetMapZoom(2, 1)").unwrap(); // the Elwynn zone map
type Answer = (Option<String>, Option<String>, f64, f64, f64, f64, f64, f64);
let hover = |s: &mut UiScript, x: f32, y: f32| {
s.eval::<Answer>(&format!("return UpdateMapHighlight({x}, {y})"))
.unwrap()
};
let name_only =
|name: &str| -> Answer { (Some(name.into()), None, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0) };
let miss: Answer = (None, None, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
// Nothing explored. (0.5, 0.5) through Elwynn's rect is cell 8801 = the Stormwind City peer:
// the neighbouring child is NAMED, and nothing else — the B360 answer, Deadwind Pass from
// Duskwood in the report.
assert_eq!(hover(&mut s, 0.5, 0.5), name_only("Stormwind City"));
// Cell 9060 = Elwynn itself: the displayed zone never names itself.
assert_eq!(hover(&mut s, 0.82, 0.82), miss);
// An empty cell, no overlay: the tail.
assert_eq!(hover(&mut s, 0.2, 0.2), miss);
// Inside Goldshire's hit rect but fogged (bit 124 clear): no name — the pre-search walks the
// REVEALED list, and cell 8929 under it is empty.
assert_eq!(hover(&mut s, 0.42, 0.7), miss);
// Reveal Northshire (bit 125 = word 3, bit 29). Its rect contains (0.5, 0.5): the overlay
// pre-search runs BEFORE the grid, so the sub-area wins over the city under the same point.
let mut explored = vec![0u32; 64];
explored[3] = 1 << 29;
s.set_world_map_explored(explored.clone());
assert_eq!(hover(&mut s, 0.5, 0.5), name_only("Northshire Valley"));
assert_eq!(hover(&mut s, 0.42, 0.7), miss, "Goldshire is still fogged");
// Reveal Goldshire too (bit 124).
explored[3] |= 1 << 28;
s.set_world_map_explored(explored.clone());
assert_eq!(hover(&mut s, 0.42, 0.7), name_only("Goldshire"));
// Only the nameless overlay revealed (bit 126 = word 3, bit 30): its rect contains (0.5, 0.5)
// but its first area resolves to nothing, so the walk passes it and the grid answers.
let mut explored = vec![0u32; 64];
explored[3] = 1 << 30;
s.set_world_map_explored(explored.clone());
assert_eq!(hover(&mut s, 0.5, 0.5), name_only("Stormwind City"));
// Nameless first, Northshire second, both revealed: the walk continues to Northshire.
explored[3] |= 1 << 29;
s.set_world_map_explored(explored);
assert_eq!(hover(&mut s, 0.5, 0.5), name_only("Northshire Valley"));
// Control: back at the continent level the hovered zone still lights up — fileName is the art
// folder and the quad geometry is non-zero.
s.run("SetMapZoom(2)").unwrap();
let (name, file, tpx, _, tx, ty, _, _) = hover(&mut s, 0.452843, 0.720880);
assert_eq!(
(name.as_deref(), file.as_deref(), tpx),
(Some("Elwynn Forest"), Some("Elwynn"), 1.0)
);
assert!(tx > 0.0 && ty > 0.0);
}
/// SetMapZoom queues WORLD_MAP_UPDATE through the pending-event queue: nothing fires inside the
/// call (a binding can't re-enter dispatch), the registered frame hears it on the next tick.
#[test]