//! Raid-target marker **overhead billboards** — wow-re `name-render-geometry-law.md` §6 //! (VERIFIED): for each unit holding a mark on the 8-slot board (decision 0434 §6, //! `GroupState::raid_targets`) and NOT carrying a live V-nameplate (the `[CGUnit+0xe60]==0` //! mutual exclusion — the plate shows its own raid-icon child instead, `vplates`), the client //! draws a separate world billboard: the anchor unit-quad LUT `(−.5,1,0),(.5,1,0),(.5,0,0), //! (−.5,0,0)` (bottom-anchored, h-centered), the 4-column `UI-RaidTargetingIcons` atlas cell //! (`col = idx&3`, `row = idx>>2`, cell 0.25), index list `{0,1,3,3,1,2}`. //! //! Seat (§6): if the unit's overhead NAME shows this frame, the marker's bottom sits **one //! line-pitch above the top of the name block** — world Z = `anchor + (lineCount + 1)·scale`; //! with no name it sits at the bare anchor. Camera-facing, world-scaled by the shared name //! height-scale law ([`crate::nameplates::height_scale`]) — same world-pass unlit/Blend state //! as the names (walls occlude; the skipped depth-write is the shared named divergence). //! //! Size (VERIFIED, wow-re `name-render-geometry-law.md` §6, commit 5e78ea75): the marker is a //! **fixed unit (1×1) world billboard** — `scale` feeds only the seat (the z-raise), NEVER the //! quad geometry. The ref builds the quad by a verbatim vertex copy of the unit LUT (no `fmul` //! on position) and billboards it through a world-matrix-cancellation chain (`0x7bca80…`) that //! re-normalizes the basis to unit length, so the on-screen size is a world/view-transform //! quantity independent of the name `scale`. (The earlier "one pitch square = `scale`·1" was //! REFUTED — that under-sized the marker ~5× for a small unit.) Drawn here at [`MARK_WORLD_SIZE`], //! a fixed world unit, camera-facing; the absolute pixel projection is the render boundary (a //! director look-call). The ref's third gate (`0x605f30()==0`) is unresolved and not reproduced. use bevy::prelude::*; use crate::entities::{overhead_anchor, BoneAttach, OverheadFallback}; use crate::nameplates::{height_scale, Nameplates}; use crate::net::GuidIndex; use crate::player::WorldCamera; use crate::ui_party::GroupState; use crate::vplates::VPlates; /// The mark atlas (4×2 icons in a 4-column grid, cell 0.25) — the same art the popup's submenu /// rows and the plate child slice. const MARK_TEXTURE: &str = "mpq://interface/targetingframe/ui-raidtargetingicons.blp"; /// The marker's world size (VERIFIED, wow-re §6 / commit 5e78ea75): a **fixed** world unit, /// independent of the name `scale`. The ref's quad is the bare unit LUT billboarded by a /// world-matrix-cancellation chain that leaves a unit-length basis; the pixel projection is the /// render boundary (a director look-call — adjust here if it reads too big/small). const MARK_WORLD_SIZE: f32 = 1.0; /// One live marker: the billboard entity, the unit it rides, and its wire icon slot. #[derive(Clone, Copy)] struct LiveMark { marker: Entity, unit: Entity, } /// The marker caches: the shared material, one tiny quad mesh per icon (lazy), and the live /// marker per board slot (wire icon 0..7). #[derive(Resource, Default)] pub(crate) struct RaidMarks { material: Option>, meshes: [Option>; 8], live: [Option; 8], } /// Marker component on a mark billboard entity (root-level, following its unit). #[derive(Component)] struct RaidMarkBillboard; /// The §6 quad for one wire icon: the LUT positions (local X ∈ [−.5, .5], Y ∈ [0, 1], bottom /// anchored), the atlas cell UVs, the ref's own index list. fn mark_mesh(icon: u32) -> Mesh { use bevy::asset::RenderAssetUsages; use bevy::mesh::{Indices, PrimitiveTopology}; let (u0, v0) = ((icon & 3) as f32 * 0.25, (icon >> 2) as f32 * 0.25); let mut mesh = Mesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::default(), ); mesh.insert_attribute( Mesh::ATTRIBUTE_POSITION, vec![ [-0.5, 1.0, 0.0], [0.5, 1.0, 0.0], [0.5, 0.0, 0.0], [-0.5, 0.0, 0.0], ], ); mesh.insert_attribute( Mesh::ATTRIBUTE_UV_0, vec![ [u0, v0], [u0 + 0.25, v0], [u0 + 0.25, v0 + 0.25], [u0, v0 + 0.25], ], ); mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, vec![[0.0, 0.0, 1.0]; 4]); mesh.insert_indices(Indices::U32(vec![0, 1, 3, 3, 1, 2])); mesh } /// The marker's world transform for `unit` this frame — the §6 seat over the shared /// anchor/scale law. Generic over the joint-globals filter like [`overhead_anchor`] (the placer /// passes a disjoint query). #[allow(clippy::too_many_arguments)] // the anchor law's full input set, like its callers fn mark_place( unit: Entity, tf: &Transform, plates: &Nameplates, facing: Quat, attach: &Query<&BoneAttach>, fallback: &Query<&OverheadFallback>, globals: &Query<&GlobalTransform, F>, mounts: &Query<(), With>, ) -> Transform { let anchor = overhead_anchor(unit, tf, attach, fallback, globals, mounts); // `scale` (the name's world height-law) drives ONLY the seat — the marker sits one // line-pitch above the top of the name block. The quad SIZE is a fixed world unit (§6: // `scale` never enters the marker geometry), not `scale`-scaled. let scale = height_scale(anchor.y - tf.translation.y); let lift = match plates.line_count(unit) { Some(lines) => (lines as f32 + 1.0) * scale, None => 0.0, }; Transform { translation: anchor + Vec3::Y * lift, rotation: facing, scale: Vec3::splat(MARK_WORLD_SIZE), } } /// Drive the markers: walk the 8-slot board, resolve each marked guid to its streamed entity /// (an out-of-range mark simply doesn't draw — the client can't render a unit it can't see), /// gate on the plate exclusion, and (re)build the billboard entities. Runs after the name /// driver (the seat reads this frame's line counts) — spawn-frame seat here, the per-frame /// re-seat is [`place_raid_marks`]. #[allow(clippy::too_many_arguments, clippy::type_complexity)] // one Bevy system's full input set fn drive_raid_marks( mut commands: Commands, group: Res, index: Res, vplates: Res, plates: Res, units: Query<&Transform>, camera: Query<&Transform, With>, anchor_q: ( Query<&BoneAttach>, Query<&OverheadFallback>, Query<&GlobalTransform>, Query<(), With>, ), mut marks: ResMut, mut meshes: ResMut>, mut materials: ResMut>, asset_server: Res, ) { let Ok(cam_tf) = camera.single() else { return; }; let facing = cam_tf.rotation; let material = marks .material .get_or_insert_with(|| { materials.add(StandardMaterial { base_color: Color::WHITE, base_color_texture: Some(asset_server.load::(MARK_TEXTURE)), // The names' world-pass state: unlit, depth-TESTED (walls occlude), Blend // (the shared skipped-depth-write divergence). unlit: true, alpha_mode: AlphaMode::Blend, cull_mode: None, ..default() }) }) .clone(); for slot in 0..8usize { let guid = group.raid_targets[slot]; let unit = (guid != 0) .then(|| index.0.get(&guid).copied()) .flatten() // The plate exclusion + a despawning unit both unmark the overlay. .filter(|e| !vplates.0.contains(e) && units.contains(*e)); let stale = match (marks.live[slot], unit) { (Some(live), Some(unit)) if live.unit == unit => continue, // placed per frame below (live, _) => live, }; if let Some(live) = stale { if let Ok(mut e) = commands.get_entity(live.marker) { e.despawn(); } marks.live[slot] = None; } let Some(unit) = unit else { continue; }; let mesh = marks.meshes[slot] .get_or_insert_with(|| meshes.add(mark_mesh(slot as u32))) .clone(); let place = units.get(unit).map(|tf| { mark_place( unit, tf, &plates, facing, &anchor_q.0, &anchor_q.1, &anchor_q.2, &anchor_q.3, ) }); let marker = commands .spawn(( Mesh3d(mesh), MeshMaterial3d(material.clone()), place.unwrap_or_default(), RaidMarkBillboard, )) .id(); marks.live[slot] = Some(LiveMark { marker, unit }); } } /// Seat every live marker from THIS frame's propagated pose (the nameplates placer's twin — /// same PostUpdate window, so a moving unit's mark never trails its name). #[allow(clippy::type_complexity)] fn place_raid_marks( marks: Res, plates: Res, camera: Query<&Transform, (With, Without)>, units: Query<&Transform, (Without, Without)>, mut mark_tfs: Query<(&mut Transform, &mut GlobalTransform), With>, anchor_q: ( Query<&BoneAttach>, Query<&OverheadFallback>, Query<&GlobalTransform, Without>, Query<(), With>, ), ) { let Ok(cam_tf) = camera.single() else { return; }; let facing = cam_tf.rotation; for live in marks.live.iter().flatten() { let (Ok(tf), Ok((mut mtf, mut mglobal))) = (units.get(live.unit), mark_tfs.get_mut(live.marker)) else { continue; // spawned this frame and not yet flushed, or unit despawning — next frame }; let place = mark_place( live.unit, tf, &plates, facing, &anchor_q.0, &anchor_q.1, &anchor_q.2, &anchor_q.3, ); *mtf = place; *mglobal = GlobalTransform::from(place); } } /// Registers the marker driver (Update, after the name driver whose line counts seat it and the /// V-plate drive whose exclusion gates it) and the per-frame placer (PostUpdate, the nameplates /// placer's window). pub(crate) struct RaidMarksPlugin; impl Plugin for RaidMarksPlugin { fn build(&self, app: &mut App) { app.init_resource::() .add_systems( Update, drive_raid_marks .after(crate::nameplates::drive_nameplates) .after(crate::vplates::VPlateSet), ) .add_systems( PostUpdate, place_raid_marks .after(bevy::transform::TransformSystems::Propagate) .before(bevy::camera::visibility::VisibilitySystems::CheckVisibility), ); } } #[cfg(test)] mod tests { use super::*; /// The §6 quad against the verified laws: the LUT positions (bottom-anchored, h-centered, /// one unit square) and the 4-column atlas cells (`col = idx&3`, `row = idx>>2`, cell 0.25) /// — skull (Lua 8 = wire 7) lands on the second row's last cell. #[test] fn mark_mesh_matches_the_lut_and_atlas_laws() { let mesh = mark_mesh(0); let pos = mesh .attribute(Mesh::ATTRIBUTE_POSITION) .and_then(|a| a.as_float3()) .unwrap() .to_vec(); assert_eq!( pos, vec![ [-0.5, 1.0, 0.0], [0.5, 1.0, 0.0], [0.5, 0.0, 0.0], [-0.5, 0.0, 0.0] ], "the 0xce875c unit-quad LUT" ); let cell = |icon: u32| { let mesh = mark_mesh(icon); let uvs = match mesh.attribute(Mesh::ATTRIBUTE_UV_0).unwrap() { bevy::mesh::VertexAttributeValues::Float32x2(v) => v.clone(), other => panic!("uv attribute shape: {other:?}"), }; uvs[0] // the TL corner names the cell }; assert_eq!(cell(0), [0.0, 0.0], "star: col 0, row 0"); assert_eq!(cell(3), [0.75, 0.0], "triangle: col 3, row 0"); assert_eq!(cell(4), [0.0, 0.25], "moon: col 0, row 1"); assert_eq!(cell(7), [0.75, 0.25], "skull: col 3, row 1"); } }