From 71653350d81cbe51d26c53f82b71c34bdede87ac Mon Sep 17 00:00:00 2001 From: popertots Date: Sun, 22 Mar 2026 00:52:06 +0000 Subject: [PATCH] fix --- src/entities/sentient/dorf.rs | 6 ++--- src/entities/tasks/components.rs | 22 +++++++--------- src/entities/tasks/executor.rs | 7 ++++-- src/entities/tasks/idle.rs | 43 +++++++++++++++++++------------- 4 files changed, 42 insertions(+), 36 deletions(-) diff --git a/src/entities/sentient/dorf.rs b/src/entities/sentient/dorf.rs index f0f7e53..aec36d4 100644 --- a/src/entities/sentient/dorf.rs +++ b/src/entities/sentient/dorf.rs @@ -9,7 +9,7 @@ use crate::world::VisibleGameEntity; use bevy::prelude::*; use bevy_rand::prelude::*; use rand::RngExt; -use smallvec::smallvec; +use std::collections::VecDeque; #[derive(Bundle)] pub struct Dorf { @@ -53,11 +53,11 @@ impl Dorf { haul_slot: HaulSlot::default(), carry_visual: CarryVisualState::new(normal_sprite, carry_sprite), task_queue: TaskQueue { - tasks: smallvec![Task::Idle { + tasks: VecDeque::from([Task::Idle { target: origin, wander_radius: 10, origin, - }], + }]), }, task_state: TaskState::Pending, } diff --git a/src/entities/tasks/components.rs b/src/entities/tasks/components.rs index 9778246..6555319 100644 --- a/src/entities/tasks/components.rs +++ b/src/entities/tasks/components.rs @@ -7,12 +7,12 @@ //! - All task data is owned — no references to world state — for cache locality. //! //! # Performance -//! - SmallVec<[Task; 4]> for queue — avoids heap alloc for typical short queues. +//! - VecDeque for queue — O(1) push/pop at both ends. //! - Task enum is #[repr(u8)] for compact storage and fast matching. //! - No dynamic dispatch — match on Task variant directly in executor. use bevy::prelude::*; -use smallvec::SmallVec; +use std::collections::VecDeque; /// A single task an entity can execute. /// @@ -88,10 +88,10 @@ impl Task { /// Tasks are pushed to the back; executor pops from front when complete. /// Higher-priority tasks can be inserted at front via `push_front`. /// -/// Uses SmallVec to avoid heap allocation for typical queue lengths (<4 tasks). +/// Uses VecDeque for O(1) push/pop at both ends. #[derive(Component, Debug, Default)] pub struct TaskQueue { - pub tasks: SmallVec<[Task; 4]>, + pub tasks: VecDeque, } impl TaskQueue { @@ -102,31 +102,27 @@ impl TaskQueue { #[inline] pub fn current(&self) -> Option<&Task> { - self.tasks.first() + self.tasks.front() } #[inline] pub fn current_mut(&mut self) -> Option<&mut Task> { - self.tasks.first_mut() + self.tasks.front_mut() } #[inline] pub fn push(&mut self, task: Task) { - self.tasks.push(task); + self.tasks.push_back(task); } #[inline] pub fn push_front(&mut self, task: Task) { - self.tasks.insert(0, task); + self.tasks.push_front(task); } #[inline] pub fn pop(&mut self) -> Option { - if self.tasks.is_empty() { - None - } else { - Some(self.tasks.remove(0)) - } + self.tasks.pop_front() } #[inline] diff --git a/src/entities/tasks/executor.rs b/src/entities/tasks/executor.rs index 516f4de..97bc601 100644 --- a/src/entities/tasks/executor.rs +++ b/src/entities/tasks/executor.rs @@ -33,8 +33,11 @@ pub fn task_executor_system( mut completed_writer: MessageWriter, mut failed_writer: MessageWriter, ) { - let Ok(mut rng) = rng_q.single_mut() else { - return; + // Get mutable access to the single GlobalRng entity + // This must be done once per frame, not per entity + let mut rng = match rng_q.single_mut() { + Ok(r) => r, + Err(_) => return, // No GlobalRng entity exists yet }; for (entity, mut queue, mut state, mut ambulatory, transform) in query.iter_mut() { diff --git a/src/entities/tasks/idle.rs b/src/entities/tasks/idle.rs index 59dd2fc..7d439ee 100644 --- a/src/entities/tasks/idle.rs +++ b/src/entities/tasks/idle.rs @@ -31,33 +31,42 @@ pub(super) fn execute_idle( let entity_pos = (transform.translation / ITILE_SIZE as f32).as_ivec3(); + // Track if target actually changed this tick + let mut target_changed = false; + // If reached target or target is no longer standable, pick new target if entity_pos == *target || !tilemap.is_standable(*target) { if let Some(new_target) = pick_wander_target(origin, *wander_radius, tilemap, rng) { *target = new_target; + target_changed = true; } } - // Set ambulatory target - pathfinding system will compute path - ambulatory.target = Some(Vec3::new( - target.x as f32, - target.y as f32, - transform.translation.z, - )); - ambulatory.current_path = None; - ambulatory.path_index = 0; + // Only reset path if target changed or no path exists + // This avoids forcing pathfinding to recompute every frame + if target_changed || ambulatory.current_path.is_none() { + ambulatory.target = Some(Vec3::new( + target.x as f32, + target.y as f32, + transform.translation.z, + )); + ambulatory.current_path = None; + ambulatory.path_index = 0; + } + // Otherwise: keep existing path, let movement system continue along it } /// Pick a random standable tile within wander radius of origin. +/// Uses reservoir sampling to avoid heap allocation. fn pick_wander_target( origin: &IVec3, radius: i32, tilemap: &TileMap, rng: &mut WyRand, ) -> Option { - let mut candidates = smallvec::SmallVec::<[IVec3; 16]>::new(); + let mut chosen = None; + let mut count = 0u32; - // Collect valid tiles within radius for dx in -radius..=radius { for dy in -radius..=radius { let candidate = IVec3::new( @@ -66,15 +75,13 @@ fn pick_wander_target( origin.z, ); if tilemap.is_standable(candidate) { - candidates.push(candidate); + count += 1; + // Reservoir sampling: 1/count chance to replace chosen + if rng.random_range(0..count) == 0 { + chosen = Some(candidate); + } } } } - - if candidates.is_empty() { - return None; - } - - let idx = rng.random_range(0..candidates.len()); - Some(candidates[idx]) + chosen }