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:
@@ -1394,19 +1394,15 @@ fn bag_slot_cooldown_sweeps_through_the_xml() {
|
||||
s.set_container(0, Some(backpack(Some((52_000, 60_000, true)))));
|
||||
s.run("OpenAllBags()").unwrap();
|
||||
s.fire_event("BAG_UPDATE", vec![benilla_ui::script::ScriptValue::Int(0)]);
|
||||
// The stock machine (decision 2019): the slot's cooldown pane, sequence 0 scrubbed by the
|
||||
// next paint's `OnUpdateModel` to the elapsed fraction.
|
||||
super::test_ui::cooldown_facts(&mut s);
|
||||
s.tick(0.0);
|
||||
s.resolve();
|
||||
|
||||
let sweep = |s: &UiScript| {
|
||||
s.extract().iter().find_map(|q| match q.content {
|
||||
QuadContent::Cooldown { fraction, .. } => Some(fraction),
|
||||
_ => None,
|
||||
})
|
||||
};
|
||||
let fraction = sweep(&s).expect("the bag slot sweeps");
|
||||
assert!(
|
||||
(fraction - 0.8).abs() < 1e-3,
|
||||
"48 of 60 s elapsed: fraction {fraction}"
|
||||
);
|
||||
let sweep = |s: &UiScript| super::test_ui::cooldown_play_any(s);
|
||||
let play = sweep(&s).expect("the bag slot sweeps");
|
||||
assert_eq!(play, (0, 800), "48 of 60 s elapsed: sequence 0 at 800 ms");
|
||||
|
||||
// The cooldown clears (a CLEAR_COOLDOWN, or it simply ran out before the re-push): the
|
||||
// refresh event re-reads the now-cold triple and hides the widget.
|
||||
@@ -1835,16 +1831,16 @@ fn a_key_dropped_on_the_button_files_itself() {
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// The item-push drop animation (decision 0887): `ITEM_PUSH(container, icon)` runs the pushed item's
|
||||
/// icon down into **that** container's bag-bar button and nobody else's, along the curves read out of
|
||||
/// `ForcedBackpackItem.m2` — pop in, hang, fall, shrink away — and hides itself at the end.
|
||||
///
|
||||
/// This is the whole observable contract of `BenillaItemPushAnim_*`: the routing (which button), the
|
||||
/// motion (starts a fall above the button, lands centred on it), the fade/scale shape (the file's
|
||||
/// keys), and the CLAMP end (one play, then gone). It is also the regression net for the OnUpdate
|
||||
/// gate — an anim frame left shown would keep ticking forever.
|
||||
/// The item-push drop animation is the reference's own `<Model>` now (decisions 2013/2015):
|
||||
/// `ITEM_PUSH(container, icon)` reaches the stock `ItemAnim_OnEvent`, which puts the icon on the
|
||||
/// pane whose parent button owns that inventory slot (`ReplaceIconTexture`), arms sequence 0 at
|
||||
/// 0 and shows it; the pane's clock runs the file's 1000 ms clamp; `ItemAnim_OnAnimFinished`
|
||||
/// hides it. The pane authored no size, so its rect is the file's own bounding box in layout
|
||||
/// units — the 42.41 × 124.72 the 0887 card measured by hand at 16:9.
|
||||
#[test]
|
||||
fn an_item_push_drops_its_icon_into_the_bag_that_took_it() {
|
||||
use benilla_ui::widget::{ModelFileFacts, SequenceFacts};
|
||||
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1600.0, 900.0);
|
||||
@@ -1853,42 +1849,50 @@ fn an_item_push_drops_its_icon_into_the_bag_that_took_it() {
|
||||
}
|
||||
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
// `ForcedBackpackItem.m2` (`m2seq`/`m2batch`): one sequence, id 0, 1000 ms, clamp; the
|
||||
// header box spans the card's whole travel band — 0.02707 × 0.07962 model units, which at
|
||||
// 16:9 (a layout unit is 768·√(a²+1) = 1566.4 FrameXML units) is the 42.41 × 124.72 rect
|
||||
// 0887's card measured.
|
||||
const CARD: &str = r"Interface\ItemAnimations\ForcedBackpackItem.mdx";
|
||||
s.set_model_facts(
|
||||
CARD,
|
||||
ModelFileFacts {
|
||||
sequences: vec![SequenceFacts {
|
||||
anim_id: 0,
|
||||
duration_ms: 1000,
|
||||
looping: false,
|
||||
}],
|
||||
bbox: ([0.0, 0.0, 0.0], [0.02707, 0.07962, 0.0]),
|
||||
},
|
||||
);
|
||||
s.resolve();
|
||||
|
||||
let shown = |s: &UiScript, name: &str| {
|
||||
s.eval::<bool>(&format!("return {name}BenillaItemPush:IsShown()"))
|
||||
s.eval::<bool>(&format!("return {name}ItemAnim:IsShown()"))
|
||||
.unwrap()
|
||||
};
|
||||
// The card's centre relative to its button's centre, in screen px (y-up): (dx, dy).
|
||||
let offset = |s: &UiScript, name: &str| -> (f32, f32) {
|
||||
s.eval::<(f32, f32)>(&format!(
|
||||
"local a, b = {name}BenillaItemPush, {name} \
|
||||
local ax, ay = a:GetLeft() + a:GetWidth() / 2, a:GetBottom() + a:GetHeight() / 2 \
|
||||
local bx, by = b:GetLeft() + b:GetWidth() / 2, b:GetBottom() + b:GetHeight() / 2 \
|
||||
return ax - bx, ay - by"
|
||||
))
|
||||
.unwrap()
|
||||
};
|
||||
let size = |s: &UiScript, name: &str| {
|
||||
s.eval::<f32>(&format!("return {name}BenillaItemPush:GetWidth()"))
|
||||
.unwrap()
|
||||
};
|
||||
let alpha = |s: &UiScript, name: &str| {
|
||||
s.eval::<f32>(&format!("return {name}BenillaItemPush:GetAlpha()"))
|
||||
.unwrap()
|
||||
};
|
||||
// The card's centre relative to its button's BOTTOMRIGHT corner — the reference's OWN frame of
|
||||
// reference for this widget (it anchors the `<Model>`'s BOTTOMRIGHT to the button's), and the
|
||||
// only one the numbers are stable in: the card's placement depends on the anchor offset and
|
||||
// the M2 bounding box, NOT on how big the button is. Measuring against the button's centre
|
||||
// instead is what let 0887's placement look plausible.
|
||||
let corner = |s: &UiScript, name: &str| -> (f32, f32) {
|
||||
s.eval::<(f32, f32)>(&format!(
|
||||
"local a, b = {name}BenillaItemPush, {name} \
|
||||
local ax, ay = a:GetLeft() + a:GetWidth() / 2, a:GetBottom() + a:GetHeight() / 2 \
|
||||
return ax - b:GetRight(), ay - b:GetBottom()"
|
||||
))
|
||||
.unwrap()
|
||||
// The shown card panes, as the renderer sees them: `(icon, play head)` per visible pane.
|
||||
type Card = (Option<String>, Option<(u16, u32)>);
|
||||
let cards = |s: &mut UiScript| -> Vec<Card> {
|
||||
let heads = s.visible_model_panes();
|
||||
s.extract()
|
||||
.into_iter()
|
||||
.filter_map(|q| match q.content {
|
||||
QuadContent::ModelPane {
|
||||
handle,
|
||||
model: Some(m),
|
||||
icon,
|
||||
..
|
||||
} if m == CARD => Some((
|
||||
icon,
|
||||
heads
|
||||
.iter()
|
||||
.find(|p| p.handle == handle)
|
||||
.and_then(|p| p.play.map(|ph| (ph.anim_id, ph.cursor_ms))),
|
||||
)),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
// Nothing is animating until a push arrives.
|
||||
@@ -1899,11 +1903,11 @@ fn an_item_push_drops_its_icon_into_the_bag_that_took_it() {
|
||||
] {
|
||||
assert!(!shown(&s, b), "{b}'s card starts hidden");
|
||||
}
|
||||
assert!(cards(&mut s).is_empty());
|
||||
|
||||
// A push into equipped bag 2. `arg1` is the reference's own vocabulary since 1751 window 3 —
|
||||
// the button's INVENTORY-slot id, which is what `ItemAnim_OnEvent` compares against — so bag 2
|
||||
// is `CharacterBag1Slot`'s 21, not the container id 2 this used to send (`ui_loot.rs`
|
||||
// `push_container`, the `0x491bb5` selector).
|
||||
// is `CharacterBag1Slot`'s 21.
|
||||
s.fire_event(
|
||||
"ITEM_PUSH",
|
||||
vec![
|
||||
@@ -1911,159 +1915,70 @@ fn an_item_push_drops_its_icon_into_the_bag_that_took_it() {
|
||||
benilla_ui::script::ScriptValue::Str("Interface\\Icons\\INV_Misc_Bag_08".into()),
|
||||
],
|
||||
);
|
||||
assert!(
|
||||
shown(&s, "CharacterBag1Slot"),
|
||||
"the card that took it plays"
|
||||
);
|
||||
assert!(
|
||||
!shown(&s, "MainMenuBarBackpackButton") && !shown(&s, "KeyRingButton"),
|
||||
"…and nobody else's"
|
||||
);
|
||||
s.resolve();
|
||||
assert!(shown(&s, "CharacterBag1Slot"), "bag 2's card plays");
|
||||
for b in [
|
||||
"MainMenuBarBackpackButton",
|
||||
"CharacterBag0Slot",
|
||||
"KeyRingButton",
|
||||
] {
|
||||
assert!(!shown(&s, b), "{b} took nothing, so {b} animates nothing");
|
||||
}
|
||||
// The pushed icon reaches the RENDERER, not just the Lua state — exactly one quad carries it.
|
||||
let drawn = |s: &UiScript, icon: &str| {
|
||||
s.extract()
|
||||
.iter()
|
||||
.filter(
|
||||
|q| matches!(&q.content, QuadContent::Texture { path: Some(p), .. } if p == icon),
|
||||
)
|
||||
.count()
|
||||
};
|
||||
let live = cards(&mut s);
|
||||
assert_eq!(
|
||||
drawn(&s, "Interface\\Icons\\INV_Misc_Bag_08"),
|
||||
1,
|
||||
"the card wears the pushed item's icon (ITEM_PUSH's arg2)"
|
||||
live,
|
||||
vec![(
|
||||
Some(r"Interface\Icons\INV_Misc_Bag_08".to_string()),
|
||||
Some((0, 0))
|
||||
)],
|
||||
"one pane, the pushed icon on it, sequence 0 at 0"
|
||||
);
|
||||
// The implicit rect: the file's box in layout units, hung off the button's BOTTOMRIGHT
|
||||
// (−10, 0) as the stock template anchors it.
|
||||
let (w, h): (f32, f32) = s
|
||||
.eval("return CharacterBag1SlotItemAnim:GetWidth(), CharacterBag1SlotItemAnim:GetHeight()")
|
||||
.unwrap();
|
||||
assert!(
|
||||
(w - 42.41).abs() < 0.05 && (h - 124.72).abs() < 0.05,
|
||||
"the card's rect is the file's box: {w} × {h}"
|
||||
);
|
||||
let (dx, dy): (f32, f32) = s
|
||||
.eval(
|
||||
"local a, b = CharacterBag1SlotItemAnim, CharacterBag1Slot \
|
||||
return a:GetRight() - b:GetRight(), a:GetBottom() - b:GetBottom()",
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
(dx + 10.0).abs() < 0.01 && dy.abs() < 0.01,
|
||||
"BOTTOMRIGHT (−10, 0) off the button: ({dx}, {dy})"
|
||||
);
|
||||
|
||||
// Every figure below is the REFERENCE's, from wow-re's §5-cross-checked
|
||||
// `modelframe-implicit-size-law.md` §3 — not a ratio we chose. The three it publishes
|
||||
// (t = 0, 0.133, 1.0) are asserted to a tenth of a pixel; the rest assert the SHAPE of the
|
||||
// motion, which is where 0887's authored placement actually went wrong.
|
||||
//
|
||||
// What changed from 0887, and why the old numbers looked right: it measured against the
|
||||
// button's CENTRE and drove both axes off one shared 0..1 "drop" curve, which happens to land
|
||||
// within ~1 px of the truth in y for a 36 px button. In x it was wrong in DIRECTION — the card
|
||||
// drifts RIGHT as it falls, and the old code had it starting left and ending centred.
|
||||
|
||||
// t=0: invisible, full size, parked high above the button. The .m2's alpha key (0.000, 0.0),
|
||||
// scale key (0.000, 1.0), translation (0.000, (0,0)).
|
||||
let (cx, cy) = corner(&s, "CharacterBag1Slot");
|
||||
assert!(
|
||||
(cx + 30.14).abs() < 0.1 && (cy - 68.99).abs() < 0.1,
|
||||
"starts at the ref's (-30.14, 68.99) from the button's bottom-right: got ({cx}, {cy})"
|
||||
);
|
||||
assert!(
|
||||
(size(&s, "CharacterBag1Slot") - 36.86).abs() < 0.1,
|
||||
"the quad is 0.0288 model units, not the 0.03 that 0887 rounded it to: got {}",
|
||||
size(&s, "CharacterBag1Slot")
|
||||
);
|
||||
assert!(
|
||||
alpha(&s, "CharacterBag1Slot") < 0.01,
|
||||
"fades in from nothing"
|
||||
);
|
||||
|
||||
// t=0.133: fully faded in, at the 1.2x swell.
|
||||
s.tick(0.133);
|
||||
s.resolve();
|
||||
assert!(
|
||||
(alpha(&s, "CharacterBag1Slot") - 1.0).abs() < 0.02,
|
||||
"opaque by the alpha track's second key"
|
||||
);
|
||||
assert!(
|
||||
(size(&s, "CharacterBag1Slot") - 44.24).abs() < 0.1,
|
||||
"swollen to the ref's 44.24 px peak: got {}",
|
||||
size(&s, "CharacterBag1Slot")
|
||||
);
|
||||
// The pop moves the centre by a twentieth of a pixel, and that tiny drift is the tell that the
|
||||
// bone's PIVOT is not the quad's centre — scaling up pushes the centre away from the pivot,
|
||||
// scaling down pulls it in, one mechanism at both ends. Folding the pivot into the centre (the
|
||||
// obvious simplification) loses this and misses the landing point by a fifth of a pixel.
|
||||
let (px, py) = corner(&s, "CharacterBag1Slot");
|
||||
assert!(
|
||||
(px + 30.18).abs() < 0.02 && (py - 68.98).abs() < 0.02,
|
||||
"the 1.2x pop drifts the centre off the pivot by the ref's (-0.046, -0.015): got ({px}, {py})"
|
||||
);
|
||||
|
||||
// t=0.267: back to the icon's own size, still opaque, still parked.
|
||||
s.tick(0.134);
|
||||
s.resolve();
|
||||
assert!(
|
||||
(size(&s, "CharacterBag1Slot") - 36.86).abs() < 0.1,
|
||||
"settled back at the scale track's third key"
|
||||
);
|
||||
let (_, cy) = corner(&s, "CharacterBag1Slot");
|
||||
assert!(
|
||||
(cy - 68.99).abs() < 0.1,
|
||||
"has not started falling: got {cy}"
|
||||
);
|
||||
|
||||
// t=0.5: STILL PARKED — the translation track's second key is (0,0), so the whole first half
|
||||
// second is a hang. The other two tracks are already running down, though: the three curves
|
||||
// keep different schedules, which is the thing a "hold everything then drop" reading would get
|
||||
// wrong (scale/alpha are 0.687/0.682 here, on the 0.267→1.000 ramps).
|
||||
s.tick(0.233);
|
||||
s.resolve();
|
||||
let (_, cy) = corner(&s, "CharacterBag1Slot");
|
||||
assert!(
|
||||
(cy - 69.0).abs() < 0.1,
|
||||
"has not moved yet at the half second: got {cy}"
|
||||
);
|
||||
assert!(
|
||||
(size(&s, "CharacterBag1Slot") - 25.31).abs() < 0.2,
|
||||
"but is already dwindling: got {}",
|
||||
size(&s, "CharacterBag1Slot")
|
||||
);
|
||||
assert!(
|
||||
(alpha(&s, "CharacterBag1Slot") - 0.682).abs() < 0.02,
|
||||
"and already fading: got {}",
|
||||
alpha(&s, "CharacterBag1Slot")
|
||||
);
|
||||
|
||||
// t=0.75: halfway through the fall — and moving RIGHT as it drops, which is the axis 0887 had
|
||||
// backwards. The translation is per-axis (+0.0104, -0.0402 model units), not one shared curve.
|
||||
s.tick(0.25);
|
||||
s.resolve();
|
||||
let (cx, cy) = corner(&s, "CharacterBag1Slot");
|
||||
assert!(
|
||||
(cx + 23.34).abs() < 0.2,
|
||||
"drifted RIGHT by half the 13.3 px total: got {cx}"
|
||||
);
|
||||
assert!(
|
||||
(cy - 43.33).abs() < 0.2,
|
||||
"half of the 51.4 px fall travelled by t=0.75: got {cy}"
|
||||
);
|
||||
|
||||
// Just short of the end: down on the button, shrunk to nothing, faded out.
|
||||
s.tick(0.24);
|
||||
s.resolve();
|
||||
let (cx, cy) = corner(&s, "CharacterBag1Slot");
|
||||
assert!(
|
||||
(cx + 16.87).abs() < 0.2 && (cy - 18.68).abs() < 0.2,
|
||||
"arrives at the ref's landing point: got ({cx}, {cy})"
|
||||
);
|
||||
// Which is also, within a couple of pixels, the button's own centre — the reference's card
|
||||
// really does drop INTO the bag button, and that is the one thing 0887 got right for the wrong
|
||||
// reason. The corner-relative assertion above is the exact one; this is the sanity check, and
|
||||
// its tolerance is a button-size term, not slack: the landing point is fixed relative to the
|
||||
// button's BOTTOMRIGHT, so the offset from its CENTRE moves by half of any size change. The
|
||||
// bar's buttons are 37x37 now — `ItemButtonTemplate`'s own size, inherited through the
|
||||
// reference's `PaperDollItemSlotButtonTemplate` — where the deleted BagFrame.xml authored 36.
|
||||
let (dx, dy) = offset(&s, "CharacterBag1Slot");
|
||||
assert!(
|
||||
dx.abs() < 2.0 && dy.abs() < 2.0,
|
||||
"lands on the button it went into: got ({dx}, {dy})"
|
||||
);
|
||||
assert!(
|
||||
size(&s, "CharacterBag1Slot") < 2.0,
|
||||
"collapsed to the scale track's 0.0144"
|
||||
);
|
||||
assert!(alpha(&s, "CharacterBag1Slot") < 0.05, "faded out");
|
||||
|
||||
// Past 1.000s the CLAMP sequence is over — the ref's OnAnimFinished → Hide.
|
||||
s.tick(0.05);
|
||||
// The clock runs the file's clamp: half a second in, the play head is at 500 ms…
|
||||
s.tick(0.5);
|
||||
assert_eq!(cards(&mut s)[0].1, Some((0, 500)));
|
||||
// …and past 1000 ms the completion fires `ItemAnim_OnAnimFinished` → `Hide()`.
|
||||
s.tick(0.55);
|
||||
assert!(
|
||||
!shown(&s, "CharacterBag1Slot"),
|
||||
"one play, then gone (and no OnUpdate left running)"
|
||||
"one play, then gone (the stock OnAnimFinished hides it)"
|
||||
);
|
||||
assert!(cards(&mut s).is_empty());
|
||||
|
||||
// A second push RESTARTS the card (SetSequenceTime(0, 0) on a fresh arm).
|
||||
s.fire_event(
|
||||
"ITEM_PUSH",
|
||||
vec![
|
||||
benilla_ui::script::ScriptValue::Int(21),
|
||||
benilla_ui::script::ScriptValue::Str("Interface\\Icons\\INV_Misc_Bag_09".into()),
|
||||
],
|
||||
);
|
||||
s.resolve();
|
||||
assert_eq!(
|
||||
cards(&mut s),
|
||||
vec![(
|
||||
Some(r"Interface\Icons\INV_Misc_Bag_09".to_string()),
|
||||
Some((0, 0))
|
||||
)]
|
||||
);
|
||||
|
||||
// The keyring is a real destination, not a rounding of the backpack.
|
||||
|
||||
Reference in New Issue
Block a user