//! World interaction foundation — *what is the player pointing at?* //! //! This is the shared base for every "point at the world" feature: the debug **object inspector** //! (today), and tomorrow hover tooltips, the contextual cursor (gear over objects, sword over //! attackable units), mouseover-targeting, and right-click-to-interact. Built once so we don't build //! picking + identity twice. //! //! Three pieces: //! - [`WorldObject`] — an **identity** component on every pickable world entity (a doodad/WMO model, a //! creature, a GameObject): its kind, a human label (model path or unit name), an id, and an optional //! detail line. Attached at the spawn sites. //! - [`MouseoverTarget`] — the resource [`update_mouseover`] fills each frame with the nearest //! `WorldObject` under the cursor, found by ray-casting against the **actual mesh geometry** (so it //! works on colliderless props — most doodads, including the campfire — which a physics raycast //! misses). //! - the **inspector surface** ([`inspect_ui`]) — a standalone, key-toggleable overlay (**Ctrl+Cmd+I**): a //! weak "armed" pill plus a small identity card that follows the cursor over any picked object. It's //! its own surface, *not* a section of the backtick debug panel, so identifying a thing costs one //! chord and no panel. //! - the **cast journal** ([`journal`]) — the *temporal* half of the same instrument: a spell is an //! event, gone before a cursor could reach it, so every cast edge is recorded as it flows past and //! the same inspector overlay lists the recent ones, click-to-copy. //! //! Picking only runs while [`InspectMode`] is on (today's sole consumer, driven by the inspector //! chord); when real player-facing consumers arrive it simply runs whenever any of them is active. use bevy::picking::mesh_picking::ray_cast::{MeshRayCast, MeshRayCastSettings, RayCastVisibility}; use bevy::platform::collections::HashSet; use bevy::prelude::*; use bevy::window::PrimaryWindow; use bevy_egui::{egui, EguiContexts, EguiPrimaryContextPass}; use crate::debug_panel::{overlay_text, ModelKind, OVERLAY_FILL, OVERLAY_TEXT_DIM}; use crate::net::ObjectStore; use crate::player::WorldCamera; use crate::ui_script::PointerOverUi; mod journal; /// The identity of a pickable world thing, read by the inspector now and by tooltips/cursor/targeting /// later. Attached to a thing's renderable mesh entities at spawn. #[derive(Component, Clone)] pub struct WorldObject { pub kind: ModelKind, /// Primary label — a model path (doodads/WMOs/GameObjects) or a unit name. Shown to the player/dev. pub label: String, /// Identifier — placement uniqueId, server guid, or display id (`0` if none). pub id: u32, /// Optional second line of kind-specific detail (e.g. `"emitters: 2"`); shown when non-empty. pub detail: String, } /// A clean left-*click* in the world — a press + release with no drag. Emitted by /// [`crate::player::control`], which is the single arbiter of click-vs-drag because it owns the /// left-drag camera orbit: a left *drag* engages the orbit and emits nothing, a left *click* emits this. /// World-interaction consumers (target selection) read it instead of re-deciding the gesture; *where* /// the cursor is is already tracked continuously (the hover pick), so this carries no position. #[derive(Message, Clone, Copy)] pub struct WorldClick; /// A clean right-*click* in the world — same arbiter and drag test as [`WorldClick`], for the right /// button (whose *drag* is the character turn). Vanilla's context action: attack a hostile under the /// cursor (later: interact/gossip on a friendly). #[derive(Message, Clone, Copy)] pub struct WorldRightClick; /// The right button's **DOWN edge** in the world — emitted at the press, before the click-vs-drag /// test even starts, whenever the press belongs to the world (in the viewport off the UI, or any /// press while a look session already owns the hidden cursor). The reference's /// `CGWorldFrame::OnMouseDown 0x483c40` analogue: ground-targeting's right-click cancel hangs off /// this edge (`0x492c20`, wow-re `world-click-targeting.md` Q3), which fires whether the press /// becomes a click OR a turn-drag, and consumes nothing (the ref handler returns 0, so the /// BUTTON2 turn and the release's context click still run). #[derive(Message, Clone, Copy)] pub struct WorldRightPress; /// Whether mouseover picking runs. Today it's armed/disarmed by the **Ctrl+Cmd+I** inspector toggle /// ([`toggle_inspect`]). #[derive(Resource, Default)] pub struct InspectMode { pub enabled: bool, } /// The nearest [`WorldObject`] under the cursor this frame, or `None`. Consumers read `entity` and look /// up its [`WorldObject`]; `point`/`distance` are the world-space hit. #[derive(Resource, Default)] pub struct MouseoverTarget { pub entity: Option, pub point: Vec3, pub distance: f32, } /// Ray-cast from the cursor into the world and record the nearest [`WorldObject`] hit. Restricted to /// entities carrying `WorldObject` (so terrain, particle billboards, and other un-identified meshes are /// transparent to the pick), and skipped entirely unless inspection is active. fn update_mouseover( inspect: Res, pointer_over_ui: Res, mut target: ResMut, window: Query<&Window, With>, camera: Query<(&Camera, &GlobalTransform), With>, objects: Query>, mut ray_cast: MeshRayCast, ) { if !inspect.enabled { if target.entity.is_some() { *target = MouseoverTarget::default(); } return; } target.entity = None; // The pointer is over the dev UI (e.g. the now-overlaid debug panel), not the world — don't pick // behind it. This replaces the old "is the cursor in the inset world viewport?" test, which no // longer means anything now the panel overlays a full-screen view. if pointer_over_ui.0 { return; } let Ok((camera, cam_tf)) = camera.single() else { return; }; let Ok(window) = window.single() else { return; }; let Some(cursor) = window.cursor_position() else { return; // cursor left the window, or we're in mouselook (hidden) }; let identified: HashSet = objects.iter().collect(); if let Some((entity, point, distance)) = pick_at_cursor(cursor, camera, cam_tf, &identified, &mut ray_cast) { target.entity = Some(entity); target.point = point; target.distance = distance; } } /// Cast a ray from the logical `cursor` position into the world and return the nearest hit among /// `pickable`: `(entity, world point, distance)`. The `pickable` set restricts the cast (terrain, /// particle billboards, and other un-identified meshes stay transparent), so callers choose *what* is /// pickable while sharing *how* — the inspector's per-frame mouseover passes every [`WorldObject`], the /// target picker passes only unit meshes (so a doodad in front doesn't block a click on a mob). pub fn pick_at_cursor( cursor: Vec2, camera: &Camera, cam_tf: &GlobalTransform, pickable: &HashSet, ray_cast: &mut MeshRayCast, ) -> Option<(Entity, Vec3, f32)> { let ray = camera.viewport_to_world(cam_tf, cursor).ok()?; let filter = |e: Entity| pickable.contains(&e); let settings = MeshRayCastSettings::default() .with_visibility(RayCastVisibility::VisibleInView) .with_filter(&filter); ray_cast .cast_ray(ray, &settings) .first() .map(|(e, hit)| (*e, hit.point, hit.distance)) } /// **Ctrl+Cmd+I** (for *inspect*) arms/disarms the inspector — the dev-instrument chord, off the /// bare-letter plane the game's own bindings own (decision 0585). Unmistakable as a chord, so unlike /// the old bare `i` it needs no chat-bar/EditBox gate. fn toggle_inspect(keys: Res>, mut inspect: ResMut) { if crate::debug_panel::dev_chord(&keys, KeyCode::KeyI) { inspect.enabled = !inspect.enabled; } } /// How long the inspector card shows its "copied to clipboard" confirmation after a left-click. const COPY_FLASH_SECS: f32 = 1.2; /// A per-kind accent so the card's header is glanceable (which *sort* of thing am I over?) before you /// even read the label. fn kind_color(kind: ModelKind) -> egui::Color32 { match kind { ModelKind::Doodad => egui::Color32::from_rgb(140, 220, 140), // green — props/trees ModelKind::Wmo => egui::Color32::from_rgb(150, 185, 240), // blue — buildings ModelKind::Creature => egui::Color32::from_rgb(240, 205, 130), // gold — NPCs ModelKind::GameObject => egui::Color32::from_rgb(220, 165, 220), // violet — GameObjects } } /// The inspector's GameObject collision readout (decision 0763): does a hull exist, is it disabled /// right now, and what stored state does the passability gate see. Named so the bundled `stores` /// param stays readable. type GoCollisionReadout = ( Has, Has, Option<&'static crate::go_anim::GoAnim>, ); /// The inspector's entity LIGHT readout (decision 0776): the lane this object's parts render /// under, and which attach found the room — "two identical GameObjects a few yards apart, one lit /// like the room and one like the street" is the report that made this a card line rather than a /// rebuild with `WOW_INTERIOR_LOG`. type EntityLightReadout = ( &'static crate::interior::InteriorAnchor, Has, ); /// The inspector overlay, drawn only while armed: a weak top-centre "armed" pill (so it's obvious the /// mode is on and how to leave it) and, whenever the cursor is over an identified object, a compact /// identity card pinned to the cursor. No chrome, no panel — its own lightweight surface. #[allow(clippy::too_many_arguments)] fn inspect_ui( mut contexts: EguiContexts, inspect: Res, mouseover: Res, objects: Query<&WorldObject>, // The pickable mesh is a child of the net entity; its descriptor store (`ObjectStore`) lives on the // parent, so the readout hops child → parent. parents: Query<&ChildOf>, // Bundled into one param (the same arity ceiling as `click_input` below): the descriptor // store + the coarse kind the line gates go by. stores: ( Query<&ObjectStore>, Query<&crate::net::NetEntity>, // The GameObject collision readout (decision 0763): whether a hull exists, whether it is // currently disabled, and the client's stored state the gate reads. Query, Query, ), guids: Query<&crate::net::Guid>, castings: Query<&crate::creature_anim::Casting>, drivers: Query<&crate::creature_anim::AnimDriver>, anim_data: Option>, spells: Option>, mut names: ResMut, net_commands: Res, // Bundled into one param (Bevy's system-function arity ceiling): the copy-click button, and // the flag it must yield to — a left press this frame the UI already consumed as a // cursor-payload world drop (0216 §3) must not ALSO land as an inspector copy-click, the same // yield every other world left-press consumer gives it (see `PointerOverUi` above for the // hover-time twin). click_input: ( Res>, Res, ), time: Res