From cb5b18008d65692b53c4318447a702f8de8a571a Mon Sep 17 00:00:00 2001 From: popertots Date: Sun, 22 Mar 2026 11:50:53 +0000 Subject: [PATCH] fix --- assets/entities/dorf.toml | 11 ++++--- assets/entities/pig.toml | 4 +-- assets/entities/rabbit.toml | 4 +-- config.toml | 6 ++-- src/entities/behaviour/registry.rs | 46 +++++++++++++++++++++++----- src/entities/sentient/dorf.rs | 4 ++- src/entities/tasks/components.rs | 4 +-- src/entities/tasks/executor.rs | 11 +++---- src/entities/tasks/idle.rs | 48 +++++++++--------------------- src/main.rs | 7 ++--- 10 files changed, 79 insertions(+), 66 deletions(-) diff --git a/assets/entities/dorf.toml b/assets/entities/dorf.toml index 8e8220b..224b803 100644 --- a/assets/entities/dorf.toml +++ b/assets/entities/dorf.toml @@ -1,11 +1,10 @@ [idle] -# Standard deviation of wander spread in world units. -# 3 chunks * CHUNK_SIZE(8) * ITILE_SIZE(16) = 384.0 -sigma_world = 384.0 +# Standard deviation of wander spread in chunks. +sigma_chunks = 3.0 # Probability (0.0-1.0) that arriving at a target triggers a loiter pause. loiter_chance = 0.6 # Loiter duration range in FixedUpdate ticks (~60 TPS). -loiter_min_ticks = 60 # ~1 second -loiter_max_ticks = 240 # ~4 seconds +loiter_min_ticks = 60 +loiter_max_ticks = 240 # Probability each of the two flip opportunities fires. -flip_chance = 0.5 \ No newline at end of file +flip_chance = 0.5 diff --git a/assets/entities/pig.toml b/assets/entities/pig.toml index 3927d9e..e3f6500 100644 --- a/assets/entities/pig.toml +++ b/assets/entities/pig.toml @@ -1,6 +1,6 @@ [idle] -sigma_world = 256.0 +sigma_chunks = 2.0 loiter_chance = 0.7 loiter_min_ticks = 90 loiter_max_ticks = 300 -flip_chance = 0.4 \ No newline at end of file +flip_chance = 0.4 diff --git a/assets/entities/rabbit.toml b/assets/entities/rabbit.toml index 620cd73..3f533f9 100644 --- a/assets/entities/rabbit.toml +++ b/assets/entities/rabbit.toml @@ -1,6 +1,6 @@ [idle] -sigma_world = 192.0 +sigma_chunks = 1.5 loiter_chance = 0.3 loiter_min_ticks = 20 loiter_max_ticks = 80 -flip_chance = 0.3 \ No newline at end of file +flip_chance = 0.3 diff --git a/config.toml b/config.toml index 356bfbd..419b4cd 100644 --- a/config.toml +++ b/config.toml @@ -1,9 +1,9 @@ -initial_chunk_radius = 5 +initial_chunk_radius = 15 [display] vsync = "mailbox" [spawn_counts] -dorfs = 50 +dorfs = 5 pigs = 5 -rabbits = 15 \ No newline at end of file +rabbits = 5 \ No newline at end of file diff --git a/src/entities/behaviour/registry.rs b/src/entities/behaviour/registry.rs index 1d2ec66..eae9b27 100644 --- a/src/entities/behaviour/registry.rs +++ b/src/entities/behaviour/registry.rs @@ -4,6 +4,8 @@ //! EntityBehaviourRegistry::load(), inserted as a Resource, and exposed //! globally via OnceLock for use in async contexts. +use crate::constants::ITILE_SIZE; +use crate::world::chunks::CHUNK_SIZE; use bevy::prelude::*; use serde::Deserialize; use std::collections::HashMap; @@ -11,8 +13,25 @@ use std::sync::OnceLock; static BEHAVIOUR_GLOBAL: OnceLock> = OnceLock::new(); +/// Deserialization shape matching assets/entities/.toml. +/// sigma_chunks is human-friendly (e.g. 3.0); converted to sigma_world +/// at load time so runtime code never deals with chunk arithmetic. +#[derive(Deserialize)] +struct IdleBehaviourToml { + sigma_chunks: f32, + loiter_chance: f32, + loiter_min_ticks: u32, + loiter_max_ticks: u32, + flip_chance: f32, +} + +#[derive(Deserialize)] +struct EntityBehaviourToml { + idle: IdleBehaviourToml, +} + /// Idle/wandering behaviour parameters for one entity type. -#[derive(Deserialize, Debug, Clone)] +#[derive(Debug, Clone)] pub struct IdleBehaviour { /// Standard deviation of Gaussian wander spread in world units. pub sigma_world: f32, @@ -27,7 +46,7 @@ pub struct IdleBehaviour { } /// All behaviour config for one entity type. -#[derive(Deserialize, Debug, Clone)] +#[derive(Debug, Clone)] pub struct EntityBehaviour { pub idle: IdleBehaviour, } @@ -44,8 +63,19 @@ impl EntityBehaviourRegistry { 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) + let toml: EntityBehaviourToml = toml::from_str(&src) .unwrap_or_else(|e| panic!("Failed to parse {}.toml: {}", name, e)); + + let sigma_world = toml.idle.sigma_chunks * CHUNK_SIZE as f32 * ITILE_SIZE as f32; + let behaviour = EntityBehaviour { + idle: IdleBehaviour { + sigma_world, + loiter_chance: toml.idle.loiter_chance, + loiter_min_ticks: toml.idle.loiter_min_ticks, + loiter_max_ticks: toml.idle.loiter_max_ticks, + flip_chance: toml.idle.flip_chance, + }, + }; map.insert(name.to_string(), behaviour); } Self(map) @@ -56,12 +86,14 @@ impl EntityBehaviourRegistry { BEHAVIOUR_GLOBAL.get_or_init(|| registry.0.clone()); } - /// Get behaviour config for an entity type. - pub fn global_get(entity_type: &str) -> EntityBehaviour { + /// Get behaviour config for an entity type. Returns a static reference — + /// zero allocation, zero copy. The &'static lifetime is valid for the + /// program's entire runtime because the OnceLock owns the map. + pub fn global_get(entity_type: &str) -> &'static EntityBehaviour { BEHAVIOUR_GLOBAL .get() - .and_then(|m| m.get(entity_type).cloned()) - .unwrap_or_else(|| panic!("No behaviour config for entity type '{}'", entity_type)) + .and_then(|m| m.get(entity_type)) + .unwrap_or_else(|| panic!("No behaviour config for '{}'", entity_type)) } } diff --git a/src/entities/sentient/dorf.rs b/src/entities/sentient/dorf.rs index 070c6c3..2e149ea 100644 --- a/src/entities/sentient/dorf.rs +++ b/src/entities/sentient/dorf.rs @@ -59,7 +59,9 @@ impl Dorf { tasks: VecDeque::from([Task::Idle { origin, sigma_world: behaviour.idle.sigma_world, - state: IdleState::Picking, + state: IdleState::Picking { + retry_after_tick: 0, + }, }]), }, task_state: TaskState::Pending, diff --git a/src/entities/tasks/components.rs b/src/entities/tasks/components.rs index 19f2113..1f9112d 100644 --- a/src/entities/tasks/components.rs +++ b/src/entities/tasks/components.rs @@ -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. diff --git a/src/entities/tasks/executor.rs b/src/entities/tasks/executor.rs index ccf49b8..6eeefdb 100644 --- a/src/entities/tasks/executor.rs +++ b/src/entities/tasks/executor.rs @@ -24,7 +24,6 @@ pub fn task_executor_system( mut commands: Commands, tilemap: Res, chunk_map: Res, - behaviour_registry: Res, mut rng_q: Query<&mut WyRand, With>, mut tick: Local, 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, + }, }); } } diff --git a/src/entities/tasks/idle.rs b/src/entities/tasks/idle.rs index d8f3c86..ae8d0a0 100644 --- a/src/entities/tasks/idle.rs +++ b/src/entities/tasks/idle.rs @@ -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/.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 { for z in -3i32..=4i32 { diff --git a/src/main.rs b/src/main.rs index eb555b2..b435cc8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ use crate::entities::item::{ initialize_item_rotation_state, item_tile_management_system, ItemRotationTimer, }; use crate::game::SpawnDelay; -use crate::plugins::{CargoPlugin, TasksPlugin, EntitiesPlugin}; +use crate::plugins::{CargoPlugin, EntitiesPlugin, TasksPlugin}; use crate::world::WorldPlugin; mod camera; @@ -34,7 +34,7 @@ fn main() { .set(WindowPlugin { primary_window: Some(Window { title: String::from("Dorf"), - present_mode: bevy::window::PresentMode::Mailbox, + present_mode: bevy::window::PresentMode::AutoVsync, ..Default::default() }), ..Default::default() @@ -69,7 +69,6 @@ fn main() { entities::shared_systems::digging::dig_system, ), ) - .insert_resource(ItemRotationTimer::default()) .add_systems(Update, initialize_item_rotation_state) .add_systems( @@ -78,4 +77,4 @@ fn main() { ) .add_systems(Update, debug::entity_dump::dump_entity_positions) .run(); -} \ No newline at end of file +}