Files
settled-reach/server/src/simulation/poi.rs
T
jpmschweitzerandClaude Opus 4.6 aa79dd97e7 fix(simulation): Clippy cleanup and CI enforcement (#635)
Fix all Clippy warnings across the server codebase (2411 insertions, 1341
deletions). Raise type-complexity-threshold to 750 and too-many-arguments
to 12 in .clippy.toml for idiomatic Bevy ECS system signatures. The server
now passes `cargo clippy -- --deny warnings` cleanly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 10:33:15 +01:00

214 lines
7.3 KiB
Rust

//! Point of Interest data model (#148).
//!
//! POIs are discoverable world locations: quest-relevant places, hidden
//! areas, landmarks, vendors, etc. They integrate with the knowledge
//! graph via `FactId("poi.*")` namespace per D-079.
//!
//! Discovery system (#149) uses `KnowledgeEventType::KnowledgeGranted`
//! with `Fact` variant to grant POI facts to observers.
use bevy_ecs::prelude::*;
use serde::{Deserialize, Serialize};
use crate::knowledge::types::FactId;
use crate::simulation::movement::TilePosition;
/// Category of point of interest. Determines client-side icon and color.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum PoiCategory {
/// Named location (dock, bar, office, residential block).
Location,
/// Vendor or service provider (fixer, medic, data broker).
Service,
/// Quest-relevant target (drop point, meeting place, evidence site).
QuestTarget,
/// Hidden area (secret passage, concealed cache, restricted zone).
Hidden,
/// Navigation landmark visible from a distance.
Landmark,
}
/// How a POI was placed in the world (content provenance).
///
/// Distinct from visibility rules: discovery_source tracks *why* the POI
/// exists; visibility tracks *how* it can be found.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum PoiDiscoverySource {
/// Part of the map template — always present on this map.
MapTemplate,
/// Procedurally generated at world creation.
Procedural,
/// Created by a quest or storyline event at runtime.
QuestGenerated,
/// Revealed by NPC testimony via knowledge grant.
NpcRevealed,
}
/// Rules governing when an observer can discover this POI.
///
/// Discovery adds `FactId("poi.{poi_id}")` to the observer's knowledge
/// graph. The discovery system (#149) evaluates these rules each tick
/// for POIs not yet known to the observer.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum PoiVisibility {
/// Discoverable when within line of sight (standard LOS rules).
LineOfSight,
/// Discoverable only within a specific tile range (Manhattan distance).
Proximity { range: u32 },
/// Not discoverable by observation. Requires a `KnowledgeGranted`
/// event from dialogue, evidence, or NPC testimony.
KnowledgeOnly,
/// Discoverable by LOS, but only if the observer already knows a
/// prerequisite fact. Example: a hidden door visible only if the
/// observer knows `"quest.secret_passage_hint"`.
RequiresFact { fact_id: String },
}
/// Point of Interest ECS component (#148).
///
/// Attached to world entities that represent discoverable locations.
/// When an observer discovers a POI, `FactId("poi.{poi_id}")` is added
/// to their `KnowledgeGraph` via the discovery system (#149).
///
/// BTreeMap ordering note: POI entities use `StableEntityId` like all
/// other entities. The `poi_id` string is for the fact namespace only.
#[derive(Component, Debug, Clone, Serialize, Deserialize)]
pub struct PointOfInterest {
/// Unique identifier within the `poi.*` fact namespace.
/// Format: `lowercase_snake_case`. Example: `"docking_bay_7"`.
/// Must be unique across all POIs in the world.
pub poi_id: String,
/// Display name shown to the player after discovery.
pub name: String,
/// World position of the POI (center tile).
pub position: TilePosition,
/// Category for client-side rendering (icon, minimap marker).
pub category: PoiCategory,
/// Content provenance — how this POI was placed in the world.
pub discovery_source: PoiDiscoverySource,
/// Rules for when/how an observer can discover this POI.
pub visibility: PoiVisibility,
}
impl PointOfInterest {
/// Generate the `FactId` for this POI in the knowledge graph.
/// Format: `"poi.{poi_id}"` per D-079 namespace convention.
pub fn fact_id(&self) -> FactId {
FactId(format!("poi.{}", self.poi_id))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_poi(id: &str, category: PoiCategory, visibility: PoiVisibility) -> PointOfInterest {
PointOfInterest {
poi_id: id.to_string(),
name: format!("Test POI {}", id),
position: TilePosition::new(10, 20, 0),
category,
discovery_source: PoiDiscoverySource::MapTemplate,
visibility,
}
}
#[test]
fn fact_id_uses_poi_namespace() {
let poi = make_poi(
"docking_bay_7",
PoiCategory::Location,
PoiVisibility::LineOfSight,
);
assert_eq!(poi.fact_id(), FactId("poi.docking_bay_7".to_string()));
}
#[test]
fn fact_id_format_is_deterministic() {
let poi1 = make_poi(
"cargo_hold",
PoiCategory::Hidden,
PoiVisibility::KnowledgeOnly,
);
let poi2 = make_poi(
"cargo_hold",
PoiCategory::Hidden,
PoiVisibility::KnowledgeOnly,
);
assert_eq!(poi1.fact_id(), poi2.fact_id());
}
#[test]
fn different_poi_ids_produce_different_fact_ids() {
let poi1 = make_poi(
"bay_alpha",
PoiCategory::Location,
PoiVisibility::LineOfSight,
);
let poi2 = make_poi(
"bay_beta",
PoiCategory::Location,
PoiVisibility::LineOfSight,
);
assert_ne!(poi1.fact_id(), poi2.fact_id());
}
#[test]
fn proximity_visibility_stores_range() {
let poi = make_poi(
"hidden_cache",
PoiCategory::Hidden,
PoiVisibility::Proximity { range: 5 },
);
match poi.visibility {
PoiVisibility::Proximity { range } => assert_eq!(range, 5),
_ => panic!("Expected Proximity visibility"),
}
}
#[test]
fn requires_fact_visibility_stores_fact_id() {
let poi = make_poi(
"secret_door",
PoiCategory::Hidden,
PoiVisibility::RequiresFact {
fact_id: "quest.secret_passage_hint".to_string(),
},
);
match &poi.visibility {
PoiVisibility::RequiresFact { fact_id } => {
assert_eq!(fact_id, "quest.secret_passage_hint");
}
_ => panic!("Expected RequiresFact visibility"),
}
}
#[test]
fn poi_categories_are_distinct() {
assert_ne!(PoiCategory::Location, PoiCategory::Service);
assert_ne!(PoiCategory::QuestTarget, PoiCategory::Hidden);
assert_ne!(PoiCategory::Hidden, PoiCategory::Landmark);
}
#[test]
fn poi_discovery_sources_are_distinct() {
assert_ne!(
PoiDiscoverySource::MapTemplate,
PoiDiscoverySource::Procedural
);
assert_ne!(
PoiDiscoverySource::QuestGenerated,
PoiDiscoverySource::NpcRevealed
);
}
#[test]
fn poi_serialization_roundtrip() {
let poi = make_poi("med_bay", PoiCategory::Service, PoiVisibility::LineOfSight);
let serialized = serde_yaml::to_string(&poi).expect("serialize");
let deserialized: PointOfInterest = serde_yaml::from_str(&serialized).expect("deserialize");
assert_eq!(deserialized.poi_id, "med_bay");
assert_eq!(deserialized.category, PoiCategory::Service);
}
}