fix: the GameObject pick skips an ineligible object, so AB flags capture
The trap gate alone could not restore capture: update_hovered_object picks the nearest geometry, then applies the mouseover-eligibility gate to that single winner. Arathi Basin's visual-only aura GameObjects (180102, type 6 TRAP) sit at the node banner's centre, so once the aura is correctly ineligible the pick returned nothing and the banner behind it was unreachable -- no cursor on any node, neutral included. Cast for every hit nearest-first and take the first the eligibility gate will publish, in both the exact and halo passes, so an ineligible front object no longer buries an interactable one behind it. The final publish gate now reuses the same predicate.
This commit is contained in:
@@ -597,6 +597,41 @@ pub(super) fn update_hovered_object(
|
||||
return;
|
||||
};
|
||||
let self_store = self_q.single().ok();
|
||||
// The mouseover-eligibility gate (`+0x54`), as a predicate over a picked entity: a GameObject
|
||||
// the cursor will never publish must not win the pick and bury the interactable behind it. A
|
||||
// battleground's visual-only aura GameObjects (Arathi Basin 180100-180102, type 6 TRAP, never
|
||||
// highlightable to a friendly or neutral player) sit at the node banner's centre, so without
|
||||
// this they take every pick and the banner is unreachable. A store not yet streamed reads
|
||||
// eligible, the gate's own default.
|
||||
let mouseover_ok = |net: Entity| -> bool {
|
||||
let Ok(store) = stores.get(net) else {
|
||||
return true;
|
||||
};
|
||||
let go_guid = guids.get(net).ok().map(|g| g.0);
|
||||
let tmpl = go_guid.and_then(|g| go_gate.templates.get(g));
|
||||
let reaction = crate::target::cursor_mode::go_reaction(
|
||||
go_gate.factions.as_deref(),
|
||||
store.0.gameobject_faction(),
|
||||
self_store,
|
||||
);
|
||||
crate::target::cursor_mode::mouseover_eligible(
|
||||
store.0.gameobject_type_id(),
|
||||
store.0.gameobject_flags(),
|
||||
store.0.gameobject_dynamic_flags(),
|
||||
tmpl.map(|t| t.highlight_column),
|
||||
reaction,
|
||||
crate::target::cursor_mode::GoOverrides {
|
||||
channel_owned: crate::target::cursor_mode::fishing_channel_owned(
|
||||
self_store, go_guid,
|
||||
),
|
||||
meeting_stone_queued: crate::target::cursor_mode::meeting_stone_queued(
|
||||
tmpl.and_then(|t| t.meeting_stone).map(|m| m.area),
|
||||
go_gate.queued_area(),
|
||||
),
|
||||
battleground: go_gate.battleground.active(),
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
// The probe's census, once a second: where the pickable GameObjects are on screen, so a
|
||||
// headless miss tells a pick fault from a camera looking elsewhere.
|
||||
@@ -628,12 +663,16 @@ pub(super) fn update_hovered_object(
|
||||
);
|
||||
}
|
||||
|
||||
// Pass 1: the exact resident geometry, nearest wins.
|
||||
let mut best: Option<(f32, Entity)> =
|
||||
benilla_world::interact::cast_pick_ray(ray, pickable, &parts, false)
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(|(e, h)| (h.distance, e));
|
||||
// Pass 1: the nearest resident geometry the eligibility gate will publish, nearest wins. Every
|
||||
// hit is asked for, nearest-first, so an ineligible front object is skipped rather than allowed
|
||||
// to bury the eligible one behind it.
|
||||
let mut best: Option<(f32, Entity)> = None;
|
||||
for (part, hit) in benilla_world::interact::cast_pick_ray(ray, pickable, &parts, true) {
|
||||
if mouseover_ok(resolve_net(part)) {
|
||||
best = Some((hit.distance, part));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2, only when nothing hit anywhere, units included (one resolve in the reference): the
|
||||
// geometry pushed 1 model unit along its normals, a halo of about a yard that makes a wispy
|
||||
@@ -648,6 +687,9 @@ pub(super) fn update_hovered_object(
|
||||
let mut best2: Option<(f32, u32, Entity)> = None;
|
||||
for (part, hit) in benilla_world::interact::cast_pick_ray_inflated(ray, pickable, &parts) {
|
||||
let net = resolve_net(part);
|
||||
if !mouseover_ok(net) {
|
||||
continue;
|
||||
}
|
||||
let prio = if *last_pick == Some(net) {
|
||||
u32::MAX // the sticky-hover cache outranks everything
|
||||
} else {
|
||||
@@ -705,34 +747,11 @@ pub(super) fn update_hovered_object(
|
||||
return;
|
||||
};
|
||||
// The mouseover-eligibility gate (vtable `+0x54` at `0x482982`): an ineligible object
|
||||
// publishes the null mouseover, so no tooltip, brighten or cursor; the reference reaches
|
||||
// `0x492890`, `0x52aa20` and `0x4945e0` only past it. Its null mouseover also hides a unit
|
||||
// behind the object, which our split picks leave hoverable.
|
||||
// publishes the null mouseover, so no tooltip, brighten or cursor. The pick above already skips
|
||||
// an ineligible object, so this normally passes; it stays as the probe's report.
|
||||
if let Ok(store) = stores.get(net_entity) {
|
||||
let tmpl = go_gate.templates.get(guid.0);
|
||||
let reaction = crate::target::cursor_mode::go_reaction(
|
||||
go_gate.factions.as_deref(),
|
||||
store.0.gameobject_faction(),
|
||||
self_store,
|
||||
);
|
||||
if !crate::target::cursor_mode::mouseover_eligible(
|
||||
store.0.gameobject_type_id(),
|
||||
store.0.gameobject_flags(),
|
||||
store.0.gameobject_dynamic_flags(),
|
||||
tmpl.map(|t| t.highlight_column),
|
||||
reaction,
|
||||
crate::target::cursor_mode::GoOverrides {
|
||||
channel_owned: crate::target::cursor_mode::fishing_channel_owned(
|
||||
self_store,
|
||||
Some(guid.0),
|
||||
),
|
||||
meeting_stone_queued: crate::target::cursor_mode::meeting_stone_queued(
|
||||
tmpl.and_then(|t| t.meeting_stone).map(|m| m.area),
|
||||
go_gate.queued_area(),
|
||||
),
|
||||
battleground: go_gate.battleground.active(),
|
||||
},
|
||||
) {
|
||||
if !mouseover_ok(net_entity) {
|
||||
if probing {
|
||||
cache.report.say(format!(
|
||||
"guid {:#x} type {} at {distance:.1} yd — hover ✗ (tmpl {:?}, highlight {:?}): \
|
||||
|
||||
Reference in New Issue
Block a user