nameplates are frames, guild names go overhead, and the camera pivots
- a nameplate is a real frame under WorldFrame, six regions and a health bar in the reference's order, so pfUI, ShaguTweaks and _Nameplates find and restyle plates instead of seeing none; the plate stays out of the mouse's way in this cut (2148) - a player's guild shows under their overhead name in angle brackets, the line an NPC's subname already used, gated by the UnitNamePlayerGuild option at its stock on (2149) - smart camera pivot is on, as 1.12 ships it: a mostly vertical drag with the camera pressed against something tilts the view instead of swinging the arm, easing home on release; in water the pivot rises for a swimmer (2149) - Follow Terrain and head bob are built and ship off, as in 1.12: Follow Terrain leans the camera up or down the slope you are walking onto, and head bob sways the first-person eye in a figure of eight while you move (2149) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,7 @@ mod model_clock;
|
||||
mod modelframe;
|
||||
mod movable;
|
||||
mod name_targets;
|
||||
mod nameplate;
|
||||
mod object_model;
|
||||
mod reference_surface;
|
||||
mod region_map;
|
||||
|
||||
@@ -0,0 +1,489 @@
|
||||
//! The nameplate ABI, as the four vanilla addons actually read it (decision 2148).
|
||||
//!
|
||||
//! These are not "does the pool work" tests. Each is a transcription of a real corpus idiom — the
|
||||
//! same walk, the same destructuring, the same identification test — so a change that would break
|
||||
//! a shipped addon fails **here** rather than on the director's screen.
|
||||
//!
|
||||
//! Sources, all `## Interface: 11200`: `pfUI/modules/nameplates.lua` (+ `compat/vanilla.lua`),
|
||||
//! `ShaguTweaks/libs/libnameplate.lua`, `CustomNameplates/CustomNameplates.lua`,
|
||||
//! `_Nameplates/_Nameplates.lua`. The byte side is wow-re
|
||||
//! `system/ui/scratch/nameplate-lua-surface.md` (§5, 2026-09-09).
|
||||
|
||||
use crate::script::{PlateGeometry, PlateState, UiScript};
|
||||
|
||||
/// A VM with a `WorldFrame` to hang plates off — the only thing [`UiScript::sync_nameplates`]
|
||||
/// requires of a session.
|
||||
fn vm() -> UiScript {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
s.run(r#"WorldFrame = CreateFrame("WorldFrame", "WorldFrame") WorldFrame:SetAllPoints()"#)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
s
|
||||
}
|
||||
|
||||
/// The plate size at the capture window (`vplates` scenario: one gx unit = 1280 px, so the
|
||||
/// 0.1 × 0.025 plate is 128 × 32 px, and at seam scale 1.0 that is 128 × 32 units).
|
||||
fn geometry() -> PlateGeometry {
|
||||
PlateGeometry {
|
||||
width: 128.0,
|
||||
height: 32.0,
|
||||
bar_off_x: 4.0,
|
||||
bar_off_y: 4.0,
|
||||
bar_width: 103.0,
|
||||
bar_height: 9.0,
|
||||
level_off_x: 11.8,
|
||||
level_off_y: 9.1,
|
||||
skull_size: 12.8,
|
||||
raid_size: 25.6,
|
||||
name_height: 12.8,
|
||||
level_height: 11.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn plate(name: &str, health: f32, max: f32) -> PlateState {
|
||||
PlateState {
|
||||
// The unit's GUID stands in as a hash of its name: what matters to these tests is that the
|
||||
// same unit keeps the same key across frames and two units never share one.
|
||||
key: name.bytes().map(u64::from).sum(),
|
||||
top_centre: (500.0, 400.0),
|
||||
health,
|
||||
max_health: max,
|
||||
bar_colour: [1.0, 0.0, 0.0],
|
||||
name: name.to_string(),
|
||||
level: Some(12),
|
||||
level_colour: [1.0, 1.0, 0.0],
|
||||
raid_icon: None,
|
||||
alpha: 1.0,
|
||||
lit: false,
|
||||
hovered: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Drive N plates and settle the layout, the way one app frame does.
|
||||
fn drive(s: &mut UiScript, states: &[PlateState]) {
|
||||
s.sync_nameplates(geometry(), states);
|
||||
s.resolve();
|
||||
}
|
||||
|
||||
/// pfUI + ShaguTweaks share one lineage: `GetNumChildren` grew, so walk the tail.
|
||||
///
|
||||
/// `initialized` is never reset in either addon, so a client whose child list SHRANK or REORDERED
|
||||
/// would make them miss every plate created after the old high-water mark — permanently, and with
|
||||
/// no error. This is the test that a retired plate keeps its slot.
|
||||
#[test]
|
||||
fn the_worldframe_child_list_only_grows_and_keeps_its_order() {
|
||||
let mut s = vm();
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return WorldFrame:GetNumChildren()").unwrap(),
|
||||
0
|
||||
);
|
||||
|
||||
drive(
|
||||
&mut s,
|
||||
&[plate("Wolf", 30.0, 40.0), plate("Bear", 10.0, 90.0)],
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return WorldFrame:GetNumChildren()").unwrap(),
|
||||
2
|
||||
);
|
||||
|
||||
// One unit leaves: the plate is HIDDEN, and the child list does not shrink.
|
||||
drive(&mut s, &[plate("Wolf", 30.0, 40.0)]);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return WorldFrame:GetNumChildren()").unwrap(),
|
||||
2
|
||||
);
|
||||
assert!(s
|
||||
.eval::<bool>(
|
||||
"local a, b = WorldFrame:GetChildren() return a:IsShown() == 1 and b:IsShown() == nil"
|
||||
)
|
||||
.unwrap());
|
||||
|
||||
// A third unit arrives: it takes the pooled plate back — the count goes to 2, not 3.
|
||||
drive(
|
||||
&mut s,
|
||||
&[plate("Wolf", 30.0, 40.0), plate("Boar", 5.0, 50.0)],
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return WorldFrame:GetNumChildren()").unwrap(),
|
||||
2
|
||||
);
|
||||
assert_eq!(s.nameplate_pool_len(), 2);
|
||||
}
|
||||
|
||||
/// pfUI's `IsNamePlate` (`nameplates.lua:70`) and ShaguTweaks' (`libnameplate.lua:12`), verbatim in
|
||||
/// shape: the object type is `"Button"`, and region #1 is a `Texture` whose `GetTexture()` is
|
||||
/// exactly `Interface\Tooltips\Nameplate-Border`.
|
||||
///
|
||||
/// `CustomNameplates` runs the texture half against **every** WorldFrame child
|
||||
/// (`CustomNameplates.lua:93`), so `GetRegions()` must also be safe to call on a frame with none.
|
||||
#[test]
|
||||
fn a_plate_identifies_by_button_plus_the_border_texture() {
|
||||
let mut s = vm();
|
||||
drive(&mut s, &[plate("Wolf", 30.0, 40.0)]);
|
||||
assert!(s
|
||||
.eval::<bool>(
|
||||
r#"
|
||||
local plate = WorldFrame:GetChildren()
|
||||
if plate:GetObjectType() ~= "Button" then return false end
|
||||
local region = plate:GetRegions()
|
||||
if not region or not region.GetTexture then return false end
|
||||
if region:GetObjectType() ~= "Texture" then return false end
|
||||
return region:GetTexture() == "Interface\\Tooltips\\Nameplate-Border"
|
||||
"#
|
||||
)
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
/// The two tuples, positionally — the ABI decision 2148 §3 records.
|
||||
///
|
||||
/// `CustomNameplates.lua:212` binds `Border, Glow, Name, Level, Boss, RaidTargetIcon`; pfUI and
|
||||
/// ShaguTweaks bind the same six by the `NAMEPLATE_OBJECTORDER` table. A **seventh** region is a
|
||||
/// bug (pfUI blanks any extra with `SetTexture("")`), and a **second** child is a bug too (pfUI
|
||||
/// destructures `healthbar, castbar` and would blank ours as a TBC cast bar).
|
||||
#[test]
|
||||
fn the_region_and_child_tuples_are_six_and_one() {
|
||||
let mut s = vm();
|
||||
drive(&mut s, &[plate("Wolf", 30.0, 40.0)]);
|
||||
|
||||
assert_eq!(
|
||||
s.eval::<i64>("local p = WorldFrame:GetChildren() return p:GetNumRegions()")
|
||||
.unwrap(),
|
||||
6
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("local p = WorldFrame:GetChildren() return p:GetNumChildren()")
|
||||
.unwrap(),
|
||||
1
|
||||
);
|
||||
assert!(s
|
||||
.eval::<bool>(
|
||||
r#"
|
||||
local p = WorldFrame:GetChildren()
|
||||
local border, glow, name, level, levelicon, raidicon = p:GetRegions()
|
||||
return border:GetTexture() == "Interface\\Tooltips\\Nameplate-Border"
|
||||
and glow:GetTexture() == "Interface\\Tooltips\\Nameplate-Glow"
|
||||
and name:GetObjectType() == "FontString"
|
||||
and level:GetObjectType() == "FontString"
|
||||
and levelicon:GetTexture() == "Interface\\TargetingFrame\\UI-TargetingFrame-Skull"
|
||||
and raidicon:GetTexture() == "Interface\\TargetingFrame\\UI-RaidTargetingIcons"
|
||||
"#
|
||||
)
|
||||
.unwrap());
|
||||
// ShaguTweaks `libnameplate.lua:39`: `plate.healthbar = plate:GetChildren()` — one child, and
|
||||
// it is the StatusBar. Its own single region is the fill `GetStatusBarTexture()` hands back.
|
||||
assert!(s
|
||||
.eval::<bool>(
|
||||
r#"
|
||||
local p = WorldFrame:GetChildren()
|
||||
local healthbar = p:GetChildren()
|
||||
if healthbar:GetObjectType() ~= "StatusBar" then return false end
|
||||
if healthbar:GetNumRegions() ~= 1 then return false end
|
||||
local fill = healthbar:GetStatusBarTexture()
|
||||
return fill:GetTexture() == "Interface\\TargetingFrame\\UI-TargetingFrame-BarFill"
|
||||
"#
|
||||
)
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
/// `_Nameplates.Nameplate.Initialize` (`_Nameplates.lua:163`) is the strictest consumer: it
|
||||
/// content-addresses the regions and **validates the two FontString anchors**, rejecting any plate
|
||||
/// whose name is not `BOTTOM`→`CENTER` and whose level is not `CENTER`→`BOTTOMRIGHT`. Those are the
|
||||
/// reference's own (ctor `0x7cb456`, bind `0x7cb7f5`).
|
||||
#[test]
|
||||
fn the_two_fontstring_anchors_are_what_nameplates_validates() {
|
||||
let mut s = vm();
|
||||
drive(&mut s, &[plate("Wolf", 30.0, 40.0)]);
|
||||
assert!(s
|
||||
.eval::<bool>(
|
||||
r#"
|
||||
local plate = WorldFrame:GetChildren()
|
||||
local Name, Level
|
||||
for _, region in ipairs({ plate:GetRegions() }) do
|
||||
if region:GetObjectType() == "FontString" then
|
||||
local point, _, relativePoint = region:GetPoint()
|
||||
if point == "BOTTOM" and relativePoint == "CENTER" then Name = region end
|
||||
if point == "CENTER" and relativePoint == "BOTTOMRIGHT" then Level = region end
|
||||
end
|
||||
end
|
||||
return Name ~= nil and Level ~= nil
|
||||
"#
|
||||
)
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
/// The bar reports **raw** health, not a fraction — `GetValue 0x78f5d0` → `[bar+0x320]`, and
|
||||
/// `GetMinMaxValues` → `(0, maxHealth)`. pfUI (`:595`) and CustomNameplates (`:471`) both divide,
|
||||
/// so a fraction here would read as full health on every plate.
|
||||
///
|
||||
/// This one corrected wow-re's own note: `nameplate-vkey.md` §4 had recorded `0x783380`, which is
|
||||
/// the internal fill *fraction*, as the Lua getter.
|
||||
#[test]
|
||||
fn the_healthbar_reports_raw_health_and_its_max() {
|
||||
let mut s = vm();
|
||||
drive(&mut s, &[plate("Wolf", 30.0, 40.0)]);
|
||||
assert!(s
|
||||
.eval::<bool>(
|
||||
r#"
|
||||
local healthbar = WorldFrame:GetChildren():GetChildren()
|
||||
local min, max = healthbar:GetMinMaxValues()
|
||||
return healthbar:GetValue() == 30 and min == 0 and max == 40
|
||||
"#
|
||||
)
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
/// The plate is anonymous. `_Nameplates.lua:164` rejects any WorldFrame child with a name, and
|
||||
/// pfUI's bubble filter does the same — a named plate is invisible to both.
|
||||
#[test]
|
||||
fn a_plate_is_anonymous() {
|
||||
let mut s = vm();
|
||||
drive(&mut s, &[plate("Wolf", 30.0, 40.0)]);
|
||||
assert!(s
|
||||
.eval::<bool>("return WorldFrame:GetChildren():GetName() == nil")
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
/// The pooling contract: a retired plate comes back as the **same Lua object**. Every addon keys
|
||||
/// its per-plate state off exactly that identity — pfUI's `registry[plate]` (`:342`),
|
||||
/// `_Nameplates.Frames[Nameplate]` (`:205`) — and re-runs `OnCreate` for an object it has not seen.
|
||||
#[test]
|
||||
fn a_retired_plate_returns_as_the_same_lua_object() {
|
||||
let mut s = vm();
|
||||
drive(&mut s, &[plate("Wolf", 30.0, 40.0)]);
|
||||
s.run("FirstPlate = WorldFrame:GetChildren()").unwrap();
|
||||
drive(&mut s, &[]);
|
||||
drive(&mut s, &[plate("Bear", 10.0, 90.0)]);
|
||||
assert!(s
|
||||
.eval::<bool>("return WorldFrame:GetChildren() == FirstPlate")
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
/// **A plate belongs to its UNIT, not to this frame's sort order.**
|
||||
///
|
||||
/// The driver hands plates over in distance-sorted order — the seat priority the reference solves
|
||||
/// overlap in — and that order changes whenever two units cross. If the pool were indexed by it,
|
||||
/// widget #1 would swap units mid-session, and every addon that caches state on the frame would be
|
||||
/// reading the other unit's: pfUI's `nameplate.original.name`/`level`, its `registry[plate]`
|
||||
/// bookkeeping, `_Nameplates`' per-plate button. So the pool is keyed by the unit's GUID, and this
|
||||
/// is the test that says so.
|
||||
#[test]
|
||||
fn a_plate_belongs_to_its_unit_across_a_sort_order_swap() {
|
||||
let mut s = vm();
|
||||
drive(
|
||||
&mut s,
|
||||
&[plate("Wolf", 30.0, 40.0), plate("Bear", 10.0, 90.0)],
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
WolfPlate = nil
|
||||
for _, p in ipairs({ WorldFrame:GetChildren() }) do
|
||||
local _, _, name = p:GetRegions()
|
||||
if name:GetText() == "Wolf" then WolfPlate = p end
|
||||
end
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(s.eval::<bool>("return WolfPlate ~= nil").unwrap());
|
||||
|
||||
// The two units cross: the driver now sorts Bear first. Nothing about either plate's identity
|
||||
// may move with it.
|
||||
drive(
|
||||
&mut s,
|
||||
&[plate("Bear", 10.0, 90.0), plate("Wolf", 30.0, 40.0)],
|
||||
);
|
||||
assert!(s
|
||||
.eval::<bool>(
|
||||
r#"
|
||||
local _, _, name = WolfPlate:GetRegions()
|
||||
return name:GetText() == "Wolf"
|
||||
"#
|
||||
)
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
/// `plate:GetAlpha() == 1` is how pfUI (`:879`) and CustomNameplates (`:287`) detect the TARGET
|
||||
/// plate: with a target up, every other plate is dimmed. So alpha is a contract, not a look.
|
||||
#[test]
|
||||
fn the_target_plate_is_the_opaque_one() {
|
||||
let mut s = vm();
|
||||
let mut target = plate("Wolf", 30.0, 40.0);
|
||||
let mut other = plate("Bear", 10.0, 90.0);
|
||||
target.alpha = 1.0;
|
||||
other.alpha = 0.5;
|
||||
drive(&mut s, &[target, other]);
|
||||
assert!(s
|
||||
.eval::<bool>(
|
||||
"local a, b = WorldFrame:GetChildren() return a:GetAlpha() == 1 and b:GetAlpha() < 1"
|
||||
)
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
/// pfUI hooks the bar's `OnValueChanged` (`:393`) and reads `this:GetParent()` inside the handler
|
||||
/// to find the plate. The driver moves that value from Rust, so the handler has to fire from there
|
||||
/// too — the reference's own bar is driven by a GUID-watch callback and fires it the same way.
|
||||
#[test]
|
||||
fn an_engine_health_write_fires_onvaluechanged_with_the_plate_as_parent() {
|
||||
let mut s = vm();
|
||||
drive(&mut s, &[plate("Wolf", 30.0, 40.0)]);
|
||||
s.run(
|
||||
r#"
|
||||
Fired = 0
|
||||
ParentIsPlate = false
|
||||
local plate = WorldFrame:GetChildren()
|
||||
local healthbar = plate:GetChildren()
|
||||
healthbar:SetScript("OnValueChanged", function()
|
||||
Fired = Fired + 1
|
||||
ParentIsPlate = (this:GetParent() == plate)
|
||||
end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
drive(&mut s, &[plate("Wolf", 12.0, 40.0)]);
|
||||
assert_eq!(s.eval::<i64>("return Fired").unwrap(), 1);
|
||||
assert!(s.eval::<bool>("return ParentIsPlate").unwrap());
|
||||
// Unmoved health fires nothing: the driver writes only what changed.
|
||||
drive(&mut s, &[plate("Wolf", 12.0, 40.0)]);
|
||||
assert_eq!(s.eval::<i64>("return Fired").unwrap(), 1);
|
||||
}
|
||||
|
||||
/// **An addon takes the plate over.** pfUI blanks all six regions (`DisableObject` →
|
||||
/// `SetTexture("")`) and paints its own art; ShaguTweaks reparents the raid icon. A driver that
|
||||
/// re-stamped every property every frame would undo that at 60 Hz — so static properties are
|
||||
/// written once, at creation, and never again.
|
||||
#[test]
|
||||
fn the_driver_does_not_overwrite_an_addons_takeover() {
|
||||
let mut s = vm();
|
||||
drive(&mut s, &[plate("Wolf", 30.0, 40.0)]);
|
||||
s.run(
|
||||
r#"
|
||||
local plate = WorldFrame:GetChildren()
|
||||
for _, region in ipairs({ plate:GetRegions() }) do
|
||||
if region.SetTexture then region:SetTexture("") end
|
||||
end
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
// Several frames of ordinary driving — health moving, the unit renamed, the plate re-seated.
|
||||
for hp in [29.0, 28.0, 27.0] {
|
||||
let mut state = plate("Wolf", hp, 40.0);
|
||||
state.top_centre = (500.0 + hp, 400.0);
|
||||
drive(&mut s, &[state]);
|
||||
}
|
||||
assert!(s
|
||||
.eval::<bool>(
|
||||
r#"
|
||||
local border = WorldFrame:GetChildren():GetRegions()
|
||||
return border:GetTexture() == "" or border:GetTexture() == nil
|
||||
"#
|
||||
)
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
/// pfUI reads `glow:IsShown()` as the MOUSEOVER signal (`:601`, `:880`). benilla does not paint the
|
||||
/// additive rim (0184 — it read as hard edge lines on our pipeline, and the director pinned a bar
|
||||
/// brighten instead), but the region is structurally present and really shown, because the
|
||||
/// deviation is about pixels and this is about data.
|
||||
#[test]
|
||||
fn the_glow_region_is_the_mouseover_signal() {
|
||||
let mut s = vm();
|
||||
drive(&mut s, &[plate("Wolf", 30.0, 40.0)]);
|
||||
assert!(s
|
||||
.eval::<bool>(
|
||||
"local _, glow = WorldFrame:GetChildren():GetRegions() return glow:IsShown() == nil"
|
||||
)
|
||||
.unwrap());
|
||||
let mut hovered = plate("Wolf", 30.0, 40.0);
|
||||
hovered.hovered = true;
|
||||
drive(&mut s, &[hovered]);
|
||||
assert!(s
|
||||
.eval::<bool>(
|
||||
"local _, glow = WorldFrame:GetChildren():GetRegions() return glow:IsShown() == 1"
|
||||
)
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
/// The level number and the skull share one seat and are mutually exclusive — `level = None` is
|
||||
/// how the driver says "world boss, or ten levels up". pfUI gates on `levelicon:IsShown()` and
|
||||
/// CustomNameplates on `Boss:IsVisible()`, so both flags have to move.
|
||||
#[test]
|
||||
fn the_skull_replaces_the_level_number() {
|
||||
let mut s = vm();
|
||||
let mut skulled = plate("Wolf", 30.0, 40.0);
|
||||
skulled.level = None;
|
||||
drive(&mut s, &[skulled]);
|
||||
assert!(s
|
||||
.eval::<bool>(
|
||||
r#"
|
||||
local _, _, _, level, levelicon = WorldFrame:GetChildren():GetRegions()
|
||||
return level:IsShown() == nil and levelicon:IsShown() == 1
|
||||
and levelicon:IsVisible() == 1
|
||||
"#
|
||||
)
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
/// An addon's own frame parented to `WorldFrame` shares the list with the plates —
|
||||
/// `_NameplatesFrame` and `pfUICombatScreen` both are — which is why the identification test in
|
||||
/// §2 is load-bearing and why the count-grew heuristic re-scans. It must not be mistaken for one.
|
||||
#[test]
|
||||
fn an_addon_frame_under_the_worldframe_is_not_a_plate() {
|
||||
let mut s = vm();
|
||||
drive(&mut s, &[plate("Wolf", 30.0, 40.0)]);
|
||||
s.run(r#"CreateFrame("Frame", "pfUICombatScreen", WorldFrame)"#)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return WorldFrame:GetNumChildren()").unwrap(),
|
||||
2
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>(
|
||||
r#"
|
||||
local found = 0
|
||||
for _, frame in ipairs({ WorldFrame:GetChildren() }) do
|
||||
local region = frame:GetRegions()
|
||||
if frame:GetObjectType() == "Button" and region and region.GetTexture
|
||||
and region:GetTexture() == "Interface\\Tooltips\\Nameplate-Border" then
|
||||
found = found + 1
|
||||
end
|
||||
end
|
||||
return found
|
||||
"#
|
||||
)
|
||||
.unwrap(),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
/// **The plate does NOT claim the UI pointer — not yet, and deliberately.**
|
||||
///
|
||||
/// The reference's plate is a mouse-enabled `Button`: hovering it publishes the mouseover
|
||||
/// (`0x7cb850` OnEnter → `0x492890`) and a click selects the unit through the button's own click
|
||||
/// slot. benilla cannot simply set that bit, because on this engine the UI pointer pass owning a
|
||||
/// frame is what makes `PointerOverUi` true, and `target::hover::update_hover` returns early on
|
||||
/// exactly that — so a mouse-enabled plate with no engine-side seam takes the hover and gives
|
||||
/// nothing back: no model brighten, no lit bar, and no selection on click, because the click never
|
||||
/// reaches the world path either.
|
||||
///
|
||||
/// So the bit stays off until the seam exists (2148 §8), and this test is the tripwire: turning it
|
||||
/// on without wiring the hover and click through the engine fails here, in the ABI suite, rather
|
||||
/// than under the director's cursor.
|
||||
#[test]
|
||||
fn a_plate_does_not_yet_claim_the_ui_pointer() {
|
||||
let mut s = vm();
|
||||
drive(&mut s, &[plate("Wolf", 30.0, 40.0)]);
|
||||
// The plate's TOP-CENTRE is (500, 400) and it hangs 32 units below, so this is its middle.
|
||||
// `mouse_move` speaks the same y-up FrameXML space the anchors do.
|
||||
//
|
||||
// The WorldFrame itself IS a legitimate hit here — it is mouse-enabled by construction, and
|
||||
// the app filters exactly that one id out (`input::feed_ui_input`, decision 1983) so camera
|
||||
// look, world clicks and hover targeting stay live over it. So the assertion mirrors the
|
||||
// app's own filter: whatever the pointer lands on must be the WorldFrame, never a plate.
|
||||
let focus = s.mouse_move(500.0, 384.0);
|
||||
assert!(
|
||||
focus.is_none_or(|id| s.is_world_frame(id)),
|
||||
"a plate claimed the UI pointer: the hover and click seams (2148 §8) have to land in the \
|
||||
same change that enables the mouse, or plate hover and click-select both die"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user