Files
benilla-octo/crates/benilla-ui/src/script/tests/worldmap.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

452 lines
19 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 world-map bindings (decision 0203 phase 2): catalog/feed pushes, the engine-owned
//! selection, and the deferred WORLD_MAP_UPDATE queue.
use super::common::script;
use crate::script::*;
/// A two-continent catalog shaped like the real 5875 one (Kalimdor first — the app's push order
/// defines every Lua-visible index; the sheet rects are the real disjoint kernel outputs). The
/// EK zone grid is sparse: exactly the real Goldshire cell (12735, verified against the shipped
/// Azeroth.zmp) marked as zone 1.
fn push_catalog(s: &mut UiScript) {
let mut ek_grid = vec![0u16; 128 * 128];
ek_grid[12735] = 1;
// 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(),
map_file: "Kalimdor".into(),
world_rect: (0.08882, 0.07910, 0.40020, 0.86952),
loc_rect: (17066.6, -19733.2, 12799.9, -11733.3),
zone_grid: Vec::new(),
zones: vec![
WorldMapZoneView {
name: "Durotar".into(),
area_id: 14,
map_file: "Durotar".into(),
loc_rect: (-1800.0, -6800.0, -3800.0, -8500.0),
overlays: Vec::new(),
},
WorldMapZoneView {
name: "The Barrens".into(),
area_id: 17,
map_file: "Barrens".into(),
loc_rect: (-1000.0, -11000.0, -5500.0, -17000.0),
overlays: Vec::new(),
},
],
},
WorldMapContinentView {
name: "Eastern Kingdoms".into(),
map_file: "Azeroth".into(),
world_rect: (0.62375, 0.02695, 0.92315, 0.87126),
loc_rect: (16000.0, -19199.9, 7466.6, -16000.0),
zone_grid: ek_grid,
zones: vec![
WorldMapZoneView {
name: "Elwynn Forest".into(),
area_id: 12,
map_file: "Elwynn".into(),
// 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),
// 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,
height: 176,
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(),
width: 172,
height: 128,
offset_x: 380,
offset_y: 320,
explore_bits: vec![124],
hit_rect: (400, 300, 520, 460),
area_name: Some("Goldshire".into()),
},
],
},
// A city peer of Elwynn — a WorldMapArea child of the continent with its own art
// folder, indistinguishable from a zone in code (wow-re 15b2a8ea Part 2a).
WorldMapZoneView {
name: "Stormwind City".into(),
area_id: 1519,
map_file: "StormwindCity".into(),
loc_rect: (-8200.0, -8900.0, -400.0, -1100.0),
overlays: Vec::new(),
},
],
},
]);
}
/// The navigation surface: lists come from the catalog, SetMapZoom's selection reads back
/// synchronously in the same Lua breath (the reference's dropdown-click contract), zone/continent
/// arguments clamp, and GetMapInfo names the displayed art folder (nil at the world level — the
/// reference Lua's own "World" fallback).
#[test]
fn worldmap_navigation_and_map_info() {
let mut s = script();
push_catalog(&mut s);
s.run(
r#"
conts = { GetMapContinents() }
kalimdor_zones = { GetMapZones(1) }
"#,
)
.unwrap();
assert_eq!(
s.eval::<(String, String)>("return conts[1], conts[2]")
.unwrap(),
("Kalimdor".into(), "Eastern Kingdoms".into()),
"display names are the Map.dbc localized ones, not the art folders"
);
assert_eq!(
s.eval::<(String, String)>("return kalimdor_zones[1], kalimdor_zones[2]")
.unwrap(),
("Durotar".into(), "The Barrens".into())
);
// World level by default: continent 0, nil map info.
assert_eq!(
s.eval::<(i64, i64)>("return GetCurrentMapContinent(), GetCurrentMapZone()")
.unwrap(),
(0, 0)
);
assert!(s.eval::<bool>("return GetMapInfo() == nil").unwrap());
// The synchronous read-back the reference relies on, and the art-folder name per level.
s.run("SetMapZoom(2)").unwrap();
assert_eq!(
s.eval::<(i64, i64, String)>(
"return GetCurrentMapContinent(), GetCurrentMapZone(), GetMapInfo()"
)
.unwrap(),
(2, 0, "Azeroth".into())
);
s.run("SetMapZoom(1, 2)").unwrap();
assert_eq!(
s.eval::<String>("return GetMapInfo()").unwrap(),
"Barrens",
"a zone selection names the ZONE's art folder"
);
// Out-of-range arguments clamp (never a Lua error — addons pass garbage).
s.run("SetMapZoom(9, 9)").unwrap();
assert_eq!(
s.eval::<(i64, i64)>("return GetCurrentMapContinent(), GetCurrentMapZone()")
.unwrap(),
(2, 2),
"continent clamps to the catalog, zone to that continent's list (EK has 2 zones)"
);
}
/// SetMapToCurrentZone lands on the app-fed player zone; the feed's projection + facing surface
/// through GetPlayerMapPosition/GetPlayerFacing — for `"player"` and, since report B320, for the
/// `party1..4` slots too. A `raid` token still answers the off-map sentinel.
#[test]
fn worldmap_current_zone_and_player_feed() {
let mut s = script();
push_catalog(&mut s);
s.set_world_map_feed(
Some((1, 2)),
Some((0.25, 0.75)),
1.5,
None,
vec![Some((0.1, 0.2)), None],
Vec::new(),
);
s.run("SetMapToCurrentZone()").unwrap();
assert_eq!(
s.eval::<(i64, i64)>("return GetCurrentMapContinent(), GetCurrentMapZone()")
.unwrap(),
(1, 2)
);
let (x, y) = s
.eval::<(f64, f64)>(r#"return GetPlayerMapPosition("player")"#)
.unwrap();
assert!((x - 0.25).abs() < 1e-6 && (y - 0.75).abs() < 1e-6);
assert!((s.eval::<f64>("return GetPlayerFacing()").unwrap() - 1.5).abs() < 1e-6);
let (px, py) = s
.eval::<(f64, f64)>(r#"return GetPlayerMapPosition("party1")"#)
.unwrap();
assert!(
(px - 0.1).abs() < 1e-6 && (py - 0.2).abs() < 1e-6,
"a party slot reads its own projection (B320)"
);
for token in ["party2", "party5", "party0", "raid1", "nonsense"] {
assert_eq!(
s.eval::<(f64, f64)>(&format!(r#"return GetPlayerMapPosition("{token}")"#))
.unwrap(),
(0.0, 0.0),
"{token} answers the off-map sentinel"
);
}
// No feed → the world sheet (never an error).
s.set_world_map_feed(None, None, 0.0, None, Vec::new(), Vec::new());
s.run("SetMapToCurrentZone()").unwrap();
assert_eq!(s.eval::<i64>("return GetCurrentMapContinent()").unwrap(), 0);
}
/// World-level ProcessMapClick picks the continent whose sheet-rect contains the click (the
/// 0x4a7100 AABB walk — the real kernel rects are disjoint); continent-level clicks resolve
/// through the 0x4a6ec0 zone grid; hover names ride the same cell law.
#[test]
fn worldmap_click_containment_and_zone_grid() {
let mut s = script();
push_catalog(&mut s);
// Mid-Kalimdor on the world sheet.
s.run("ProcessMapClick(0.25, 0.45)").unwrap();
assert_eq!(s.eval::<i64>("return GetCurrentMapContinent()").unwrap(), 1);
// Kalimdor ships no grid in this fixture: continent-level clicks are inert.
s.run("ProcessMapClick(0.5, 0.5)").unwrap();
assert_eq!(
s.eval::<(i64, i64)>("return GetCurrentMapContinent(), GetCurrentMapZone()")
.unwrap(),
(1, 0)
);
// Back at the world level, EK land picks continent 2.
s.run("SetMapZoom(0)").unwrap();
s.run("ProcessMapClick(0.72, 0.63)").unwrap();
assert_eq!(s.eval::<i64>("return GetCurrentMapContinent()").unwrap(), 2);
// Continent level: Goldshire's UV inside the EK rect lands on grid cell 12735 (the real
// Azeroth.zmp index for that world position) → zone 1. The zone lights up: name + fileName +
// the six geometry values (wow-re 15b2a8ea continent branch), all against the zone's loc rect
// within the continent. cont_w=35199.9, cont_h=23466.6; w=2000, h=1500.
let (name, file, tpx, tpy, tx, ty, sx, sy) = s
.eval::<(String, String, f64, f64, f64, f64, f64, f64)>(
"return UpdateMapHighlight(0.452843, 0.720880)",
)
.unwrap();
assert_eq!(name, "Elwynn Forest");
assert_eq!(
file, "Elwynn",
"fileName = the zone art folder → <file>Highlight"
);
assert_eq!(
tpx, 1.0,
"continent-branch texPercentageX is the constant 1.0"
);
assert!(
(tpy - 0.75).abs() < 1e-6,
"texPercentageY = dim/potdim = 96/128 = 0.75, got {tpy}"
);
assert!(
(tx - 2000.0 / 35199.9).abs() < 1e-4,
"textureX = w/contW, got {tx}"
);
assert!(
(ty - 1500.0 / 23466.6).abs() < 1e-4,
"textureY = h/contH, got {ty}"
);
assert!(
(sx - 24000.0 / 35199.9).abs() < 1e-4,
"scrollChildX = (contX0-zoneX0)/contW, got {sx}"
);
assert!(
(sy - 7866.6 / 23466.6).abs() < 1e-4,
"scrollChildY = (contY0-zoneY0)/contH, got {sy}"
);
// Ocean at continent level: off the grid → the nil/zero tail (frame hides the quad).
assert!(s
.eval::<Option<String>>("return UpdateMapHighlight(0.99, 0.01)")
.unwrap()
.is_none());
s.run("ProcessMapClick(0.452843, 0.720880)").unwrap();
assert_eq!(
s.eval::<(i64, i64)>("return GetCurrentMapContinent(), GetCurrentMapZone()")
.unwrap(),
(2, 1),
"the grid click drills into the zone"
);
// Now at the ZONE level (Elwynn): a click re-expressed through ELWYNN's own rect lands on
// cell 8801 = the Stormwind City peer child → drills into the city map. This is the path that
// was a no-op before (wow-re 15b2a8ea Part 2: continent- and zone-level clicks run identical
// code, only the windowing rect differs).
s.run("ProcessMapClick(0.5, 0.5)").unwrap();
assert_eq!(
s.eval::<(i64, i64)>("return GetCurrentMapContinent(), GetCurrentMapZone()")
.unwrap(),
(2, 2),
"a zone-level click on a city footprint drills into the city"
);
// Open ocean at the world level: outside both rects, no selection change.
s.run("SetMapZoom(0)").unwrap();
s.run("ProcessMapClick(0.01, 0.99)").unwrap();
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]
fn worldmap_update_event_fires_on_next_tick() {
let mut s = script();
push_catalog(&mut s);
// Drain the catalog push's own queued event first.
s.tick(0.01);
s.run(
r#"
heard = 0
f = CreateFrame("Frame", "MapListener")
f:RegisterEvent("WORLD_MAP_UPDATE")
f:SetScript("OnEvent", function() heard = heard + 1 end)
SetMapZoom(1)
"#,
)
.unwrap();
assert_eq!(
s.eval::<i64>("return heard").unwrap(),
0,
"nothing fires inside the SetMapZoom call itself"
);
s.tick(0.01);
assert_eq!(s.eval::<i64>("return heard").unwrap(), 1);
s.tick(0.01);
assert_eq!(
s.eval::<i64>("return heard").unwrap(),
1,
"the queue drains — no re-fire"
);
}
/// The exploration fog: overlays reveal per the pushed bitset — none before any push, the
/// matching subset after (bit 125 = Northshire in the fixture), the full info tuple comes back,
/// and re-pushing the same bitset queues no extra repaint.
#[test]
fn worldmap_overlays_reveal_by_explored_bits() {
let mut s = script();
push_catalog(&mut s);
s.run("SetMapZoom(2, 1)").unwrap(); // the Elwynn zone map (EK's only fixture zone)
// Nothing explored: the zone shows no overlays (all parchment).
assert_eq!(s.eval::<i64>("return GetNumMapOverlays()").unwrap(), 0);
// Bit 125 set (word 3, bit 29): Northshire reveals, Goldshire stays fogged.
let mut explored = vec![0u32; 64];
explored[3] = 1 << 29;
s.set_world_map_explored(explored);
assert_eq!(s.eval::<i64>("return GetNumMapOverlays()").unwrap(), 1);
let (tex, w, h, ox, oy) = s
.eval::<(String, i64, i64, i64, i64)>("return GetMapOverlayInfo(1)")
.unwrap();
assert_eq!(tex, "Interface\\WorldMap\\Elwynn\\NORTHSHIREVALLEY");
assert_eq!((w, h, ox, oy), (200, 176, 490, 200));
// Both bits: both overlays, catalog order; out-of-range asks read nil.
let mut explored = vec![0u32; 64];
explored[3] = (1 << 29) | (1 << 28);
s.set_world_map_explored(explored);
assert_eq!(s.eval::<i64>("return GetNumMapOverlays()").unwrap(), 2);
assert!(s
.eval::<bool>("return GetMapOverlayInfo(3) == nil")
.unwrap());
// At the continent level the overlay family reads empty (fog is a zone-map thing).
s.run("SetMapZoom(2)").unwrap();
assert_eq!(s.eval::<i64>("return GetNumMapOverlays()").unwrap(), 0);
}