fix(simulation): PR #66 review — dedup vision, spawn components, doc fixes

- vision.rs: deduplicate own-tile entity iteration, add same-tile test
- spawn.rs: add NpcVisionState, NpcMemory, PlayerAwareness to content-
  spawned NPCs (matching generate_npc)
- pressure.rs: update who_knows_full_scan doc to reflect actual call
  frequency, document O(N) acceptability at call site
- types.rs: fix stale protocol version comment (13→14)
- generate.rs: format!() → .to_string() (clippy)
- vision.rs: hardcoded 10 → TICKS_PER_GAME_MINUTE
- relationships.rs: fix comment "15%" → "20%" to match code
- pressure.rs: document total() floor-truncation
- save_state.rs: document NpcMemory exclusion as intentional

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-25 10:04:18 +01:00
co-authored by Claude Opus 4.6
parent abd1657d94
commit 768a431c38
8 changed files with 81 additions and 13 deletions
+7 -2
View File
@@ -82,9 +82,12 @@ pub struct CharacterPressure {
}
impl CharacterPressure {
/// Total pressure as a weighted average of all axes (0100).
/// Total pressure as a simple average of all axes (0100).
///
/// Uses integer division — remainders are floor-truncated (D-010, no floats).
/// Maximum rounding error is 2 units (e.g. axis sum 101 → 33 instead of 33.67).
pub fn total(&self) -> i32 {
// Simple average, clamped. Integer arithmetic only (D-010).
// Simple average, clamped. Integer division truncates toward zero (D-010).
((self.exposure + self.institutional + self.relationship) / 3).clamp(0, 100)
}
@@ -178,6 +181,8 @@ pub fn update_character_pressure(
// --- Relationship pressure ---
let player_stable = registry.to_stable(player_entity);
if let Some(player_sid) = player_stable {
// O(N) over all relationship edges — called once per game-minute, not every tick.
// Acceptable at v0.1 NPC counts (<100 NPCs = <100 edge iterations).
let hostile_edges = relationship_graph
.who_knows_full_scan(&player_sid)
.iter()
+2
View File
@@ -31,6 +31,8 @@
//! - Full ECS world extraction/injection (system not yet written)
//! - Pathfinding state (reconstructed from position + routine)
//! - Tier transitions in-flight (dropped to background state on load)
//! - `NpcMemory` (intentionally excluded — stale inferences would be wrong after
//! reload; memory degrades naturally over time so reset-on-load is acceptable)
use serde::{Deserialize, Serialize};
+22
View File
@@ -21,6 +21,15 @@ pub trait SpatialIndex: Send + Sync {
/// Return all entities at the exact `position`.
fn entities_at(&self, position: &TilePosition) -> Vec<Entity>;
/// Return all entities within Manhattan distance `radius` of `position`,
/// **including** entities exactly at `position`. Single-pass alternative to
/// `entities_in_range` + `entities_at`.
fn entities_within(&self, position: &TilePosition, radius: u32) -> Vec<Entity> {
let mut result = self.entities_in_range(position, radius);
result.extend(self.entities_at(position));
result
}
/// Insert or update an entity's position in the index.
fn update(&mut self, entity: Entity, position: TilePosition);
@@ -78,6 +87,19 @@ impl SpatialIndex for NaiveSpatialIndex {
.collect()
}
fn entities_within(&self, position: &TilePosition, radius: u32) -> Vec<Entity> {
self.entries
.iter()
.filter(|(_, pos)| {
pos == position
|| pos
.manhattan_distance(position)
.is_some_and(|d| d <= radius)
})
.map(|(entity, _)| *entity)
.collect()
}
fn update(&mut self, entity: Entity, position: TilePosition) {
if let Some(entry) = self.entries.iter_mut().find(|(e, _)| *e == entity) {
entry.1 = position;