This commit is contained in:
2026-03-22 11:50:53 +00:00
parent 2f456f80b8
commit cb5b18008d
10 changed files with 79 additions and 66 deletions
+2 -2
View File
@@ -17,8 +17,8 @@ use std::collections::VecDeque;
/// Sub-state of Task::Idle.
#[derive(Clone, Debug, PartialEq)]
pub enum IdleState {
/// Pick a new target this tick.
Picking,
/// Pick a new target. Retry cooldown prevents CPU spike when no tiles found.
Picking { retry_after_tick: u32 },
/// Walking toward target.
Moving { target: IVec3 },
/// Standing still, loitering.
+6 -5
View File
@@ -24,7 +24,6 @@ pub fn task_executor_system(
mut commands: Commands,
tilemap: Res<TileMap>,
chunk_map: Res<ChunkMap>,
behaviour_registry: Res<EntityBehaviourRegistry>,
mut rng_q: Query<&mut WyRand, With<GlobalRng>>,
mut tick: Local<u32>,
mut query: Query<(
@@ -53,12 +52,13 @@ pub fn task_executor_system(
let origin = transform.translation.as_ivec3();
let behaviour = EntityBehaviourRegistry::global_get(entity_type.0);
// If queue empty, assign Idle with Gaussian params from TOML
if queue.is_empty() {
queue.push(Task::Idle {
origin,
sigma_world: behaviour.idle.sigma_world,
state: IdleState::Picking,
state: IdleState::Picking {
retry_after_tick: 0,
},
});
}
@@ -138,12 +138,13 @@ pub fn task_executor_system(
});
}
// If terminal task completed, push Idle with Gaussian params
if completed_task.is_terminal() && queue.is_empty() {
queue.push(Task::Idle {
origin,
sigma_world: behaviour.idle.sigma_world,
state: IdleState::Picking,
state: IdleState::Picking {
retry_after_tick: 0,
},
});
}
}
+14 -34
View File
@@ -1,20 +1,3 @@
//! Idle task — Gaussian wander with loiter behaviour.
//!
//! # Wander model
//! Targets are selected from all standable tiles across loaded chunks,
//! weighted by Gaussian falloff from origin. Closer tiles are more likely;
//! distant tiles occasionally chosen. No hard radius cutoff.
//!
//! # Loiter model
//! On arrival there is a configurable chance of entering a loiter pause.
//! During loiter the entity stands still and may flip their sprite direction
//! once or twice to simulate "looking around". Duration and flip timing are
//! randomised within configured bounds.
//!
//! # Configuration
//! All tuning values come from assets/entities/<type>.toml via
//! EntityBehaviourRegistry. No constants in this file.
use crate::constants::{ITILE_SIZE, TILE_SIZE};
use crate::entities::behaviour::IdleBehaviour;
use crate::entities::shared_components::Ambulatory;
@@ -26,8 +9,6 @@ use bevy::prelude::*;
use bevy_rand::prelude::*;
use rand::RngExt;
/// Execute one tick of Task::Idle.
/// Called by task_executor_system when current task is Idle.
pub(super) fn execute_idle(
task: &mut Task,
transform: &Transform,
@@ -49,7 +30,11 @@ pub(super) fn execute_idle(
};
match state {
IdleState::Picking => {
IdleState::Picking { retry_after_tick } => {
if current_tick < *retry_after_tick {
return;
}
match pick_gaussian_target(origin, *sigma_world, tilemap, chunk_map, rng) {
Some(target) => {
ambulatory.target = Some(Vec3::new(
@@ -62,8 +47,7 @@ pub(super) fn execute_idle(
*state = IdleState::Moving { target };
}
None => {
// No standable tile found yet (early startup, chunk loading).
// Leave ambulatory.target as-is, try again next tick.
*retry_after_tick = current_tick.saturating_add(30);
}
}
}
@@ -75,9 +59,9 @@ pub(super) fn execute_idle(
let arrive_threshold_sq = (TILE_SIZE * 1.5) * (TILE_SIZE * 1.5);
let arrived = dist_sq < arrive_threshold_sq;
let stuck = ambulatory.target.is_none() && ambulatory.current_path.is_none();
let no_nav = ambulatory.target.is_none() && ambulatory.current_path.is_none();
if arrived || stuck {
if arrived || no_nav {
ambulatory.target = None;
ambulatory.current_path = None;
@@ -104,7 +88,9 @@ pub(super) fn execute_idle(
next_flip_at,
};
} else {
*state = IdleState::Picking;
*state = IdleState::Picking {
retry_after_tick: 0,
};
}
}
}
@@ -126,7 +112,9 @@ pub(super) fn execute_idle(
if *ticks_remaining == 0 {
sprite.flip_x = false;
*state = IdleState::Picking;
*state = IdleState::Picking {
retry_after_tick: 0,
};
} else {
*ticks_remaining -= 1;
}
@@ -134,11 +122,6 @@ pub(super) fn execute_idle(
}
}
/// Pick a target tile using Gaussian-weighted reservoir sampling across all
/// loaded chunks. Tiles closer to `origin` are exponentially more likely.
///
/// Returns a standable world-unit tile position, or None if no candidate found
/// (chunk not yet loaded, or all tiles above the search z range).
fn pick_gaussian_target(
origin: &IVec3,
sigma_world: f32,
@@ -194,9 +177,6 @@ fn pick_gaussian_target(
chosen
}
/// Find the standable tile at a given XY world position by scanning z levels.
/// Returns the tile one above the highest floor tile (where entity stands).
/// Returns None if no standable surface found in range.
#[inline]
fn find_surface(world_x: i32, world_y: i32, tilemap: &TileMap) -> Option<IVec3> {
for z in -3i32..=4i32 {