//! Perception query trait (D-017). //! //! Abstraction for perception mode geometry computation. Each mode //! (natural vision, thermal, EM, etc.) implements PerceptionQuery to //! provide mode-specific FOV and visibility sector computation. //! v0.1 implements only NaturalVision. //! //! Note: HashMap is used for `sector_lookup` — a per-frame scratch buffer //! looked up only by key. Iteration order is irrelevant here. Not subject to //! the simulation determinism constraint (see server/.clippy.toml). #![allow(clippy::disallowed_types)] use std::collections::{BTreeSet, HashMap}; use bevy_ecs::prelude::*; use crate::bridge::types::{FacingDirection, TileKind, VisibilitySector, VisibleTile}; use crate::perception::shadowcast::compute_fov; use crate::perception::vision_cone::{apply_vision_cone, VisionConeConfig}; use crate::simulation::movement::{TilePosition, WalkabilityMap}; /// Cached FOV geometry for the current frame. Produced by /// compute_visibility_geometry, consumed by compute_observer_snapshot. /// D-017 perception modes swap the geometry producer while the consumer /// remains unchanged. #[derive(Resource, Default)] pub struct VisibilityGeometry { pub visible_tiles: Vec, pub visible_positions: BTreeSet<(i32, i32)>, pub sector_lookup: HashMap<(i32, i32), VisibilitySector>, pub observer_z: i32, } /// Trait for perception mode geometry computation (D-017). /// /// Each perception mode implements this to produce a VisibilityGeometry /// from the observer's position and facing. v0.1 only implements /// NaturalVision; D-017 adds Thermal, EM, etc. pub trait PerceptionQuery: Send + Sync { fn compute_geometry( &self, observer_pos: &TilePosition, facing: FacingDirection, walkability: &WalkabilityMap, ) -> VisibilityGeometry; } /// Natural vision — default perception mode. /// Uses symmetric shadowcasting (D-011) + directional vision cone (D-015). pub struct NaturalVision; impl PerceptionQuery for NaturalVision { fn compute_geometry( &self, observer_pos: &TilePosition, facing: FacingDirection, walkability: &WalkabilityMap, ) -> VisibilityGeometry { let config = VisionConeConfig::default(); let z = observer_pos.z; let fov = compute_fov( |x, y| !walkability.can_move_to(&TilePosition::new(x, y, z)), observer_pos.x, observer_pos.y, config.forward_range, z, ); let cone_tiles = apply_vision_cone(&fov, observer_pos.x, observer_pos.y, facing, &config); let mut visible_tiles: Vec = cone_tiles .iter() .map(|&(x, y, sector)| { let tile_kind = if walkability.can_move_to(&TilePosition::new(x, y, z)) { TileKind::Floor } else { TileKind::Wall }; VisibleTile { x, y, z, visibility: sector, tile_kind, zone_id: None, } }) .collect(); visible_tiles.sort_by_key(|t| (t.x, t.y)); let visible_positions = cone_tiles.iter().map(|&(x, y, _)| (x, y)).collect(); let sector_lookup = cone_tiles .iter() .map(|&(x, y, sector)| ((x, y), sector)) .collect(); VisibilityGeometry { visible_tiles, visible_positions, sector_lookup, observer_z: z, } } } /// Resource wrapping the active perception mode (D-017). /// Defaults to NaturalVision. Swap this resource to change perception modes. #[derive(Resource)] pub struct ActivePerceptionMode(pub Box); impl Default for ActivePerceptionMode { fn default() -> Self { Self(Box::new(NaturalVision)) } }