diff --git a/crates/benilla-app/src/target/hover.rs b/crates/benilla-app/src/target/hover.rs index 08676324..36c49503 100644 --- a/crates/benilla-app/src/target/hover.rs +++ b/crates/benilla-app/src/target/hover.rs @@ -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 {:?}): \