Gaussian idle
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
pub mod registry;
|
||||
|
||||
pub use registry::{EntityBehaviourRegistry, EntityType, IdleBehaviour};
|
||||
@@ -0,0 +1,70 @@
|
||||
//! Per-entity behaviour configuration loaded from assets/entities/*.toml.
|
||||
//!
|
||||
//! Follows the DropTableRegistry pattern — loaded once at startup via
|
||||
//! EntityBehaviourRegistry::load(), inserted as a Resource, and exposed
|
||||
//! globally via OnceLock for use in async contexts.
|
||||
|
||||
use bevy::prelude::*;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
static BEHAVIOUR_GLOBAL: OnceLock<HashMap<String, EntityBehaviour>> = OnceLock::new();
|
||||
|
||||
/// Idle/wandering behaviour parameters for one entity type.
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct IdleBehaviour {
|
||||
/// Standard deviation of Gaussian wander spread in world units.
|
||||
pub sigma_world: f32,
|
||||
/// Probability (0.0–1.0) of entering loiter on arrival.
|
||||
pub loiter_chance: f32,
|
||||
/// Minimum loiter duration in FixedUpdate ticks.
|
||||
pub loiter_min_ticks: u32,
|
||||
/// Maximum loiter duration in FixedUpdate ticks.
|
||||
pub loiter_max_ticks: u32,
|
||||
/// Probability each flip opportunity fires during loiter.
|
||||
pub flip_chance: f32,
|
||||
}
|
||||
|
||||
/// All behaviour config for one entity type.
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct EntityBehaviour {
|
||||
pub idle: IdleBehaviour,
|
||||
}
|
||||
|
||||
/// Registry of all loaded entity behaviours, keyed by entity type name.
|
||||
#[derive(Resource)]
|
||||
pub struct EntityBehaviourRegistry(pub HashMap<String, EntityBehaviour>);
|
||||
|
||||
impl EntityBehaviourRegistry {
|
||||
/// Load all entity TOML files at startup.
|
||||
pub fn load() -> Self {
|
||||
let mut map = HashMap::new();
|
||||
for name in &["dorf", "rabbit", "pig"] {
|
||||
let path = format!("assets/entities/{}.toml", name);
|
||||
let src = std::fs::read_to_string(&path)
|
||||
.unwrap_or_else(|_| panic!("assets/entities/{}.toml not found", name));
|
||||
let behaviour: EntityBehaviour = toml::from_str(&src)
|
||||
.unwrap_or_else(|e| panic!("Failed to parse {}.toml: {}", name, e));
|
||||
map.insert(name.to_string(), behaviour);
|
||||
}
|
||||
Self(map)
|
||||
}
|
||||
|
||||
/// Initialise the global OnceLock for async/non-ECS access.
|
||||
pub fn init_global(registry: &EntityBehaviourRegistry) {
|
||||
BEHAVIOUR_GLOBAL.get_or_init(|| registry.0.clone());
|
||||
}
|
||||
|
||||
/// Get behaviour config for an entity type.
|
||||
pub fn global_get(entity_type: &str) -> EntityBehaviour {
|
||||
BEHAVIOUR_GLOBAL
|
||||
.get()
|
||||
.and_then(|m| m.get(entity_type).cloned())
|
||||
.unwrap_or_else(|| panic!("No behaviour config for entity type '{}'", entity_type))
|
||||
}
|
||||
}
|
||||
|
||||
/// Marker component identifying an entity's type for behaviour lookup.
|
||||
#[derive(Component, Debug, Clone, Copy)]
|
||||
pub struct EntityType(pub &'static str);
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::config::GameConfig;
|
||||
use crate::constants::TILE_SIZE;
|
||||
use crate::constants::*;
|
||||
use crate::entities::behaviour::EntityType;
|
||||
use crate::entities::shared_components::Ambulatory;
|
||||
use crate::game::SpawnDelay;
|
||||
use crate::world::VisibleGameEntity;
|
||||
@@ -14,7 +15,7 @@ pub struct Pig {
|
||||
sprite: Sprite,
|
||||
transform: Transform,
|
||||
visibility: Visibility,
|
||||
// Pigs have no inventory — PersonalInventory is not attached to this entity.
|
||||
entity_type: EntityType,
|
||||
}
|
||||
|
||||
impl Pig {
|
||||
@@ -40,6 +41,7 @@ impl Pig {
|
||||
},
|
||||
transform: Transform::from_translation(position).with_scale(Vec3::splat(PIXEL_RATIO)),
|
||||
visibility: Visibility::Hidden,
|
||||
entity_type: EntityType("pig"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::config::GameConfig;
|
||||
use crate::constants::*;
|
||||
use crate::entities::behaviour::EntityType;
|
||||
use crate::entities::shared_components::Ambulatory;
|
||||
use crate::entities::shared_systems::constants::DEFAULT_DIG_INTERVAL_SECS;
|
||||
use crate::entities::shared_systems::digging::Digger;
|
||||
@@ -16,6 +17,7 @@ pub struct Rabbit {
|
||||
transform: Transform,
|
||||
visibility: Visibility,
|
||||
digger: Digger,
|
||||
entity_type: EntityType,
|
||||
}
|
||||
|
||||
impl Rabbit {
|
||||
@@ -42,6 +44,7 @@ impl Rabbit {
|
||||
transform: Transform::from_translation(position).with_scale(Vec3::splat(PIXEL_RATIO)),
|
||||
visibility: Visibility::Hidden,
|
||||
digger: Digger::new(DEFAULT_DIG_INTERVAL_SECS),
|
||||
entity_type: EntityType("rabbit"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod behaviour;
|
||||
pub mod cargo;
|
||||
pub mod item;
|
||||
pub mod livestock;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use crate::config::GameConfig;
|
||||
use crate::constants::TILE_SIZE;
|
||||
use crate::constants::*;
|
||||
use crate::entities::behaviour::{EntityBehaviourRegistry, EntityType};
|
||||
use crate::entities::cargo::{CarryVisualState, HaulSlot};
|
||||
use crate::entities::shared_components::Ambulatory;
|
||||
use crate::entities::tasks::{Task, TaskQueue, TaskState};
|
||||
use crate::entities::tasks::{IdleState, Task, TaskQueue, TaskState};
|
||||
use crate::game::SpawnDelay;
|
||||
use crate::world::VisibleGameEntity;
|
||||
use bevy::prelude::*;
|
||||
@@ -21,6 +22,7 @@ pub struct Dorf {
|
||||
carry_visual: CarryVisualState,
|
||||
task_queue: TaskQueue,
|
||||
task_state: TaskState,
|
||||
entity_type: EntityType,
|
||||
}
|
||||
|
||||
impl Dorf {
|
||||
@@ -30,6 +32,7 @@ impl Dorf {
|
||||
let normal_sprite = asset_server.load("dorf.png");
|
||||
let carry_sprite = asset_server.load("dorf.png");
|
||||
let origin = position.as_ivec3();
|
||||
let behaviour = EntityBehaviourRegistry::global_get("dorf");
|
||||
|
||||
Dorf {
|
||||
ambulatory: Ambulatory {
|
||||
@@ -54,12 +57,13 @@ impl Dorf {
|
||||
carry_visual: CarryVisualState::new(normal_sprite, carry_sprite),
|
||||
task_queue: TaskQueue {
|
||||
tasks: VecDeque::from([Task::Idle {
|
||||
target: origin,
|
||||
wander_radius: 10,
|
||||
origin,
|
||||
sigma_world: behaviour.idle.sigma_world,
|
||||
state: IdleState::Picking,
|
||||
}]),
|
||||
},
|
||||
task_state: TaskState::Pending,
|
||||
entity_type: EntityType("dorf"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,24 @@
|
||||
use bevy::prelude::*;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
/// Sub-state of Task::Idle.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum IdleState {
|
||||
/// Pick a new target this tick.
|
||||
Picking,
|
||||
/// Walking toward target.
|
||||
Moving { target: IVec3 },
|
||||
/// Standing still, loitering.
|
||||
Loitering {
|
||||
/// Ticks remaining in loiter.
|
||||
ticks_remaining: u32,
|
||||
/// Remaining sprite-flip opportunities (starts at 0, 1, or 2).
|
||||
flips_remaining: u8,
|
||||
/// Tick count at which the next flip fires.
|
||||
next_flip_at: u32,
|
||||
},
|
||||
}
|
||||
|
||||
/// A single task an entity can execute.
|
||||
///
|
||||
/// Tasks are self-contained: they carry all data needed for execution.
|
||||
@@ -27,17 +45,14 @@ use std::collections::VecDeque;
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[repr(u8)]
|
||||
pub enum Task {
|
||||
/// Idle/wandering behaviour. Refactored from existing wandering system.
|
||||
/// Target is a standable tile within wander_radius; entity moves toward it.
|
||||
/// When reached, picks new random target. Never "completes" — cycles forever
|
||||
/// until replaced by a higher-priority task.
|
||||
Idle {
|
||||
/// Current wander target tile (world position).
|
||||
target: IVec3,
|
||||
/// Max distance from origin to wander (Chebyshev).
|
||||
wander_radius: i32,
|
||||
/// Origin tile for wander bounds.
|
||||
/// Home position in world units — centre of Gaussian distribution.
|
||||
/// Set at spawn, does not drift.
|
||||
origin: IVec3,
|
||||
/// Standard deviation in world units. Read from EntityBehaviourRegistry.
|
||||
sigma_world: f32,
|
||||
/// Current execution sub-state.
|
||||
state: IdleState,
|
||||
},
|
||||
|
||||
/// Move to a tile within "close enough" threshold.
|
||||
|
||||
@@ -6,14 +6,15 @@
|
||||
//! 3. On completion/failure, update TaskState, fire event, pop task
|
||||
//! 4. If queue empty after pop, assign default Task::Idle
|
||||
//!
|
||||
//! Uses Changed<TaskQueue> + Changed<TaskState> to minimize queries.
|
||||
//! Uses Changed<TaskQueue> + Changed<TaskState> to minimise queries.
|
||||
|
||||
use crate::constants::ITILE_SIZE;
|
||||
use crate::entities::behaviour::{EntityBehaviourRegistry, EntityType};
|
||||
use crate::entities::shared_components::Ambulatory;
|
||||
use crate::entities::tasks::components::{Task, TaskQueue, TaskState};
|
||||
use crate::entities::tasks::components::{IdleState, Task, TaskQueue, TaskState};
|
||||
use crate::entities::tasks::events::{TaskClaimed, TaskCompleted, TaskFailed};
|
||||
use crate::entities::tasks::idle::execute_idle;
|
||||
use crate::world::tiles::tilemap::TileMap;
|
||||
use crate::world::chunks::ChunkMap;
|
||||
use crate::world::tiles::TileMap;
|
||||
use bevy::prelude::*;
|
||||
use bevy_rand::prelude::*;
|
||||
use rand::{RngExt, SeedableRng};
|
||||
@@ -22,13 +23,18 @@ use rand::{RngExt, SeedableRng};
|
||||
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<(
|
||||
Entity,
|
||||
&mut TaskQueue,
|
||||
&mut TaskState,
|
||||
&mut Ambulatory,
|
||||
&Transform,
|
||||
&mut Sprite,
|
||||
&EntityType,
|
||||
)>,
|
||||
mut claimed_writer: MessageWriter<TaskClaimed>,
|
||||
mut completed_writer: MessageWriter<TaskCompleted>,
|
||||
@@ -38,14 +44,21 @@ pub fn task_executor_system(
|
||||
return;
|
||||
};
|
||||
|
||||
for (entity, mut queue, mut state, mut ambulatory, transform) in query.iter_mut() {
|
||||
// If queue empty, assign default Idle task
|
||||
*tick = tick.wrapping_add(1);
|
||||
let current_tick = *tick;
|
||||
|
||||
for (entity, mut queue, mut state, mut ambulatory, transform, mut sprite, entity_type) in
|
||||
query.iter_mut()
|
||||
{
|
||||
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() {
|
||||
let origin = transform.translation.as_ivec3();
|
||||
queue.push(Task::Idle {
|
||||
target: origin,
|
||||
wander_radius: 64,
|
||||
origin,
|
||||
sigma_world: behaviour.idle.sigma_world,
|
||||
state: IdleState::Picking,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -66,20 +79,22 @@ pub fn task_executor_system(
|
||||
match current_task {
|
||||
Task::Idle { .. } => {
|
||||
execute_idle(
|
||||
entity,
|
||||
current_task,
|
||||
transform,
|
||||
&mut ambulatory,
|
||||
&mut sprite,
|
||||
&tilemap,
|
||||
&chunk_map,
|
||||
&behaviour.idle,
|
||||
&mut rng,
|
||||
current_tick,
|
||||
);
|
||||
}
|
||||
Task::GoTo {
|
||||
target,
|
||||
threshold_tiles,
|
||||
} => {
|
||||
let entity_pos = (transform.translation / ITILE_SIZE as f32).as_ivec3();
|
||||
// Chebyshev distance (max of absolute differences)
|
||||
let entity_pos = (transform.translation / 16.0f32).as_ivec3();
|
||||
let distance = (entity_pos.x - target.x)
|
||||
.abs()
|
||||
.max((entity_pos.y - target.y).abs());
|
||||
@@ -87,7 +102,6 @@ pub fn task_executor_system(
|
||||
if distance <= *threshold_tiles || !tilemap.is_standable(*target) {
|
||||
*state = TaskState::Completed;
|
||||
} else {
|
||||
// Set target - pathfinding handles the rest
|
||||
ambulatory.target = Some(Vec3::new(
|
||||
target.x as f32,
|
||||
target.y as f32,
|
||||
@@ -96,7 +110,6 @@ pub fn task_executor_system(
|
||||
ambulatory.current_path = None;
|
||||
}
|
||||
}
|
||||
// Stage 3 placeholders - log and fail for now
|
||||
Task::ChopTree { .. } | Task::HaulObject { .. } | Task::DropHauled { .. } => {
|
||||
debug!(
|
||||
"Task {} unimplemented for entity {:?}",
|
||||
@@ -125,13 +138,12 @@ pub fn task_executor_system(
|
||||
});
|
||||
}
|
||||
|
||||
// If terminal task completed, ensure Idle is queued
|
||||
// If terminal task completed, push Idle with Gaussian params
|
||||
if completed_task.is_terminal() && queue.is_empty() {
|
||||
let origin = (transform.translation / ITILE_SIZE as f32).as_ivec3();
|
||||
queue.push(Task::Idle {
|
||||
target: origin,
|
||||
wander_radius: 64,
|
||||
origin,
|
||||
sigma_world: behaviour.idle.sigma_world,
|
||||
state: IdleState::Picking,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+184
-73
@@ -1,101 +1,212 @@
|
||||
//! Idle task implementation — wandering behaviour refactored here.
|
||||
//! Idle task — Gaussian wander with loiter behaviour.
|
||||
//!
|
||||
//! This module contains the logic that was previously in the wandering
|
||||
//! system. Now it's called by the task executor when Task::Idle is active.
|
||||
//! # 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;
|
||||
use crate::entities::tasks::components::Task;
|
||||
use crate::world::tiles::tilemap::TileMap;
|
||||
use crate::entities::tasks::components::{IdleState, Task};
|
||||
use crate::world::chunks::ChunkMap;
|
||||
use crate::world::chunks::CHUNK_SIZE;
|
||||
use crate::world::tiles::TileMap;
|
||||
use bevy::prelude::*;
|
||||
use bevy_rand::prelude::*;
|
||||
use rand::RngExt;
|
||||
|
||||
/// Execute Idle task: set target for wandering, let pathfinding handle movement.
|
||||
/// Execute one tick of Task::Idle.
|
||||
/// Called by task_executor_system when current task is Idle.
|
||||
pub(super) fn execute_idle(
|
||||
entity: Entity,
|
||||
task: &mut Task,
|
||||
transform: &Transform,
|
||||
ambulatory: &mut Ambulatory,
|
||||
sprite: &mut Sprite,
|
||||
tilemap: &TileMap,
|
||||
chunk_map: &ChunkMap,
|
||||
behaviour: &IdleBehaviour,
|
||||
rng: &mut WyRand,
|
||||
current_tick: u32,
|
||||
) {
|
||||
let Task::Idle {
|
||||
target,
|
||||
wander_radius,
|
||||
origin,
|
||||
sigma_world,
|
||||
state,
|
||||
} = task
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let entity_pos = transform.translation.as_ivec3();
|
||||
|
||||
// Track if target actually changed this tick
|
||||
let mut target_changed = false;
|
||||
|
||||
// Use distance threshold for arrival check - equality never triggers due to float truncation
|
||||
let dist_sq = (transform.translation.x - target.x as f32).powi(2)
|
||||
+ (transform.translation.y - target.y as f32).powi(2);
|
||||
let arrived = dist_sq < (TILE_SIZE * 1.5) * (TILE_SIZE * 1.5);
|
||||
|
||||
// If reached target or target is no longer standable, pick new target
|
||||
if arrived || !tilemap.is_standable(*target) {
|
||||
if let Some(new_target) = pick_wander_target(origin, *wander_radius, tilemap, rng) {
|
||||
*target = new_target;
|
||||
target_changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Only reset path if target changed or no path exists
|
||||
if target_changed || ambulatory.current_path.is_none() {
|
||||
// +1.0 sub-tile offset matching original wandering system
|
||||
ambulatory.target = Some(Vec3::new(
|
||||
target.x as f32,
|
||||
target.y as f32,
|
||||
target.z as f32 + 1.0,
|
||||
));
|
||||
ambulatory.current_path = None;
|
||||
ambulatory.path_index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Pick a random standable tile within wander radius of origin.
|
||||
/// Uses reservoir sampling to avoid heap allocation.
|
||||
/// Searches for actual surface z at each XY position (like original wandering system).
|
||||
fn pick_wander_target(
|
||||
origin: &IVec3,
|
||||
radius: i32,
|
||||
tilemap: &TileMap,
|
||||
rng: &mut WyRand,
|
||||
) -> Option<IVec3> {
|
||||
let mut chosen = None;
|
||||
let mut count = 0u32;
|
||||
|
||||
for dx in -radius..=radius {
|
||||
for dy in -radius..=radius {
|
||||
// Search for actual surface z at this XY - same logic as original wandering
|
||||
for z in -3i32..=4i32 {
|
||||
let floor_pos = IVec3::new(
|
||||
origin.x + dx * ITILE_SIZE,
|
||||
origin.y + dy * ITILE_SIZE,
|
||||
z * ITILE_SIZE,
|
||||
);
|
||||
// Check if there's a floor tile at this z
|
||||
if tilemap.floor_tiles.get(&floor_pos).is_some() {
|
||||
// Check if standable one tile above
|
||||
let above = IVec3::new(floor_pos.x, floor_pos.y, floor_pos.z + ITILE_SIZE);
|
||||
if tilemap.is_standable(above) {
|
||||
count += 1;
|
||||
// Reservoir sampling: 1/count chance to replace chosen
|
||||
if rng.random_range(0..count) == 0 {
|
||||
chosen = Some(above);
|
||||
}
|
||||
break; // found surface at this XY, stop z search
|
||||
}
|
||||
match state {
|
||||
IdleState::Picking => {
|
||||
match pick_gaussian_target(origin, *sigma_world, tilemap, chunk_map, rng) {
|
||||
Some(target) => {
|
||||
ambulatory.target = Some(Vec3::new(
|
||||
target.x as f32,
|
||||
target.y as f32,
|
||||
target.z as f32 + 1.0,
|
||||
));
|
||||
ambulatory.current_path = None;
|
||||
ambulatory.path_index = 0;
|
||||
*state = IdleState::Moving { target };
|
||||
}
|
||||
None => {
|
||||
// No standable tile found yet (early startup, chunk loading).
|
||||
// Leave ambulatory.target as-is, try again next tick.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IdleState::Moving { target } => {
|
||||
let dx = transform.translation.x - target.x as f32;
|
||||
let dy = transform.translation.y - target.y as f32;
|
||||
let dist_sq = dx * dx + dy * dy;
|
||||
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();
|
||||
|
||||
if arrived || stuck {
|
||||
ambulatory.target = None;
|
||||
ambulatory.current_path = None;
|
||||
|
||||
let loiter_roll: f32 = rng.random();
|
||||
if loiter_roll < behaviour.loiter_chance {
|
||||
let duration_range = behaviour.loiter_max_ticks - behaviour.loiter_min_ticks;
|
||||
let duration =
|
||||
behaviour.loiter_min_ticks + rng.random_range(0..=duration_range);
|
||||
|
||||
let flip1: f32 = rng.random();
|
||||
let flip2: f32 = rng.random();
|
||||
let flips_remaining = (flip1 < behaviour.flip_chance) as u8
|
||||
+ (flip2 < behaviour.flip_chance) as u8;
|
||||
|
||||
let next_flip_at = if flips_remaining > 0 {
|
||||
current_tick.saturating_add(rng.random_range(1..=duration / 2))
|
||||
} else {
|
||||
u32::MAX
|
||||
};
|
||||
|
||||
*state = IdleState::Loitering {
|
||||
ticks_remaining: duration,
|
||||
flips_remaining,
|
||||
next_flip_at,
|
||||
};
|
||||
} else {
|
||||
*state = IdleState::Picking;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IdleState::Loitering {
|
||||
ticks_remaining,
|
||||
flips_remaining,
|
||||
next_flip_at,
|
||||
} => {
|
||||
if *flips_remaining > 0 && current_tick >= *next_flip_at {
|
||||
sprite.flip_x = !sprite.flip_x;
|
||||
*flips_remaining -= 1;
|
||||
|
||||
if *flips_remaining > 0 && *ticks_remaining > 2 {
|
||||
*next_flip_at =
|
||||
current_tick.saturating_add(rng.random_range(1..=*ticks_remaining / 2));
|
||||
}
|
||||
}
|
||||
|
||||
if *ticks_remaining == 0 {
|
||||
sprite.flip_x = false;
|
||||
*state = IdleState::Picking;
|
||||
} else {
|
||||
*ticks_remaining -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
tilemap: &TileMap,
|
||||
chunk_map: &ChunkMap,
|
||||
rng: &mut WyRand,
|
||||
) -> Option<IVec3> {
|
||||
let two_sigma_sq = 2.0 * sigma_world * sigma_world;
|
||||
let mut chosen: Option<IVec3> = None;
|
||||
let mut weight_sum = 0.0f32;
|
||||
|
||||
for &chunk_pos in chunk_map.loaded_chunks.keys() {
|
||||
let has_all_neighbours = chunk_map
|
||||
.loaded_chunks
|
||||
.contains_key(&(chunk_pos + IVec2::X))
|
||||
&& chunk_map
|
||||
.loaded_chunks
|
||||
.contains_key(&(chunk_pos - IVec2::X))
|
||||
&& chunk_map
|
||||
.loaded_chunks
|
||||
.contains_key(&(chunk_pos + IVec2::Y))
|
||||
&& chunk_map
|
||||
.loaded_chunks
|
||||
.contains_key(&(chunk_pos - IVec2::Y));
|
||||
if !has_all_neighbours {
|
||||
continue;
|
||||
}
|
||||
|
||||
const SAMPLES_PER_CHUNK: usize = 4;
|
||||
for _ in 0..SAMPLES_PER_CHUNK {
|
||||
let local_x = rng.random_range(0..CHUNK_SIZE);
|
||||
let local_y = rng.random_range(0..CHUNK_SIZE);
|
||||
let world_x = (chunk_pos.x * CHUNK_SIZE + local_x) * ITILE_SIZE;
|
||||
let world_y = (chunk_pos.y * CHUNK_SIZE + local_y) * ITILE_SIZE;
|
||||
|
||||
let Some(candidate) = find_surface(world_x, world_y, tilemap) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let dx = (candidate.x - origin.x) as f32;
|
||||
let dy = (candidate.y - origin.y) as f32;
|
||||
let dist_sq = dx * dx + dy * dy;
|
||||
let weight = (-dist_sq / two_sigma_sq).exp();
|
||||
|
||||
weight_sum += weight;
|
||||
let accept: f32 = rng.random();
|
||||
if accept < weight / weight_sum {
|
||||
chosen = Some(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
let floor_pos = IVec3::new(world_x, world_y, z * ITILE_SIZE);
|
||||
if tilemap.floor_tiles.contains_key(&floor_pos) {
|
||||
let above = IVec3::new(world_x, world_y, floor_pos.z + ITILE_SIZE);
|
||||
if tilemap.is_standable(above) {
|
||||
return Some(above);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ pub mod events;
|
||||
pub mod executor;
|
||||
pub mod idle;
|
||||
|
||||
pub use components::{Task, TaskQueue, TaskState};
|
||||
pub use components::{IdleState, Task, TaskQueue, TaskState};
|
||||
pub use events::{TaskBlocked, TaskClaimed, TaskCompleted, TaskDropped, TaskFailed};
|
||||
pub use executor::task_executor_system;
|
||||
|
||||
|
||||
+5
-1
@@ -36,7 +36,11 @@ impl Plugin for WorldPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
let drop_registry = crate::entities::item::drop_table::DropTableRegistry::load();
|
||||
crate::entities::item::drop_table::DropTableRegistry::init_global(&drop_registry);
|
||||
app.insert_resource(drop_registry)
|
||||
app.insert_resource(drop_registry);
|
||||
|
||||
let behaviour_registry = crate::entities::behaviour::EntityBehaviourRegistry::load();
|
||||
crate::entities::behaviour::EntityBehaviourRegistry::init_global(&behaviour_registry);
|
||||
app.insert_resource(behaviour_registry)
|
||||
.init_resource::<ChunkMap>()
|
||||
.init_resource::<TerrainBlobStorage>()
|
||||
.init_resource::<tiles::TilemapBenchmark>()
|
||||
|
||||
Reference in New Issue
Block a user