fix
This commit is contained in:
@@ -1,11 +1,10 @@
|
|||||||
[idle]
|
[idle]
|
||||||
# Standard deviation of wander spread in world units.
|
# Standard deviation of wander spread in chunks.
|
||||||
# 3 chunks * CHUNK_SIZE(8) * ITILE_SIZE(16) = 384.0
|
sigma_chunks = 3.0
|
||||||
sigma_world = 384.0
|
|
||||||
# Probability (0.0-1.0) that arriving at a target triggers a loiter pause.
|
# Probability (0.0-1.0) that arriving at a target triggers a loiter pause.
|
||||||
loiter_chance = 0.6
|
loiter_chance = 0.6
|
||||||
# Loiter duration range in FixedUpdate ticks (~60 TPS).
|
# Loiter duration range in FixedUpdate ticks (~60 TPS).
|
||||||
loiter_min_ticks = 60 # ~1 second
|
loiter_min_ticks = 60
|
||||||
loiter_max_ticks = 240 # ~4 seconds
|
loiter_max_ticks = 240
|
||||||
# Probability each of the two flip opportunities fires.
|
# Probability each of the two flip opportunities fires.
|
||||||
flip_chance = 0.5
|
flip_chance = 0.5
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
[idle]
|
[idle]
|
||||||
sigma_world = 256.0
|
sigma_chunks = 2.0
|
||||||
loiter_chance = 0.7
|
loiter_chance = 0.7
|
||||||
loiter_min_ticks = 90
|
loiter_min_ticks = 90
|
||||||
loiter_max_ticks = 300
|
loiter_max_ticks = 300
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[idle]
|
[idle]
|
||||||
sigma_world = 192.0
|
sigma_chunks = 1.5
|
||||||
loiter_chance = 0.3
|
loiter_chance = 0.3
|
||||||
loiter_min_ticks = 20
|
loiter_min_ticks = 20
|
||||||
loiter_max_ticks = 80
|
loiter_max_ticks = 80
|
||||||
|
|||||||
+3
-3
@@ -1,9 +1,9 @@
|
|||||||
initial_chunk_radius = 5
|
initial_chunk_radius = 15
|
||||||
|
|
||||||
[display]
|
[display]
|
||||||
vsync = "mailbox"
|
vsync = "mailbox"
|
||||||
|
|
||||||
[spawn_counts]
|
[spawn_counts]
|
||||||
dorfs = 50
|
dorfs = 5
|
||||||
pigs = 5
|
pigs = 5
|
||||||
rabbits = 15
|
rabbits = 5
|
||||||
@@ -4,6 +4,8 @@
|
|||||||
//! EntityBehaviourRegistry::load(), inserted as a Resource, and exposed
|
//! EntityBehaviourRegistry::load(), inserted as a Resource, and exposed
|
||||||
//! globally via OnceLock for use in async contexts.
|
//! globally via OnceLock for use in async contexts.
|
||||||
|
|
||||||
|
use crate::constants::ITILE_SIZE;
|
||||||
|
use crate::world::chunks::CHUNK_SIZE;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -11,8 +13,25 @@ use std::sync::OnceLock;
|
|||||||
|
|
||||||
static BEHAVIOUR_GLOBAL: OnceLock<HashMap<String, EntityBehaviour>> = OnceLock::new();
|
static BEHAVIOUR_GLOBAL: OnceLock<HashMap<String, EntityBehaviour>> = OnceLock::new();
|
||||||
|
|
||||||
|
/// Deserialization shape matching assets/entities/<type>.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.
|
/// Idle/wandering behaviour parameters for one entity type.
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct IdleBehaviour {
|
pub struct IdleBehaviour {
|
||||||
/// Standard deviation of Gaussian wander spread in world units.
|
/// Standard deviation of Gaussian wander spread in world units.
|
||||||
pub sigma_world: f32,
|
pub sigma_world: f32,
|
||||||
@@ -27,7 +46,7 @@ pub struct IdleBehaviour {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// All behaviour config for one entity type.
|
/// All behaviour config for one entity type.
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct EntityBehaviour {
|
pub struct EntityBehaviour {
|
||||||
pub idle: IdleBehaviour,
|
pub idle: IdleBehaviour,
|
||||||
}
|
}
|
||||||
@@ -44,8 +63,19 @@ impl EntityBehaviourRegistry {
|
|||||||
let path = format!("assets/entities/{}.toml", name);
|
let path = format!("assets/entities/{}.toml", name);
|
||||||
let src = std::fs::read_to_string(&path)
|
let src = std::fs::read_to_string(&path)
|
||||||
.unwrap_or_else(|_| panic!("assets/entities/{}.toml not found", name));
|
.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));
|
.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);
|
map.insert(name.to_string(), behaviour);
|
||||||
}
|
}
|
||||||
Self(map)
|
Self(map)
|
||||||
@@ -56,12 +86,14 @@ impl EntityBehaviourRegistry {
|
|||||||
BEHAVIOUR_GLOBAL.get_or_init(|| registry.0.clone());
|
BEHAVIOUR_GLOBAL.get_or_init(|| registry.0.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get behaviour config for an entity type.
|
/// Get behaviour config for an entity type. Returns a static reference —
|
||||||
pub fn global_get(entity_type: &str) -> EntityBehaviour {
|
/// 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
|
BEHAVIOUR_GLOBAL
|
||||||
.get()
|
.get()
|
||||||
.and_then(|m| m.get(entity_type).cloned())
|
.and_then(|m| m.get(entity_type))
|
||||||
.unwrap_or_else(|| panic!("No behaviour config for entity type '{}'", entity_type))
|
.unwrap_or_else(|| panic!("No behaviour config for '{}'", entity_type))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,9 @@ impl Dorf {
|
|||||||
tasks: VecDeque::from([Task::Idle {
|
tasks: VecDeque::from([Task::Idle {
|
||||||
origin,
|
origin,
|
||||||
sigma_world: behaviour.idle.sigma_world,
|
sigma_world: behaviour.idle.sigma_world,
|
||||||
state: IdleState::Picking,
|
state: IdleState::Picking {
|
||||||
|
retry_after_tick: 0,
|
||||||
|
},
|
||||||
}]),
|
}]),
|
||||||
},
|
},
|
||||||
task_state: TaskState::Pending,
|
task_state: TaskState::Pending,
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ use std::collections::VecDeque;
|
|||||||
/// Sub-state of Task::Idle.
|
/// Sub-state of Task::Idle.
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub enum IdleState {
|
pub enum IdleState {
|
||||||
/// Pick a new target this tick.
|
/// Pick a new target. Retry cooldown prevents CPU spike when no tiles found.
|
||||||
Picking,
|
Picking { retry_after_tick: u32 },
|
||||||
/// Walking toward target.
|
/// Walking toward target.
|
||||||
Moving { target: IVec3 },
|
Moving { target: IVec3 },
|
||||||
/// Standing still, loitering.
|
/// Standing still, loitering.
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ pub fn task_executor_system(
|
|||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
tilemap: Res<TileMap>,
|
tilemap: Res<TileMap>,
|
||||||
chunk_map: Res<ChunkMap>,
|
chunk_map: Res<ChunkMap>,
|
||||||
behaviour_registry: Res<EntityBehaviourRegistry>,
|
|
||||||
mut rng_q: Query<&mut WyRand, With<GlobalRng>>,
|
mut rng_q: Query<&mut WyRand, With<GlobalRng>>,
|
||||||
mut tick: Local<u32>,
|
mut tick: Local<u32>,
|
||||||
mut query: Query<(
|
mut query: Query<(
|
||||||
@@ -53,12 +52,13 @@ pub fn task_executor_system(
|
|||||||
let origin = transform.translation.as_ivec3();
|
let origin = transform.translation.as_ivec3();
|
||||||
let behaviour = EntityBehaviourRegistry::global_get(entity_type.0);
|
let behaviour = EntityBehaviourRegistry::global_get(entity_type.0);
|
||||||
|
|
||||||
// If queue empty, assign Idle with Gaussian params from TOML
|
|
||||||
if queue.is_empty() {
|
if queue.is_empty() {
|
||||||
queue.push(Task::Idle {
|
queue.push(Task::Idle {
|
||||||
origin,
|
origin,
|
||||||
sigma_world: behaviour.idle.sigma_world,
|
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() {
|
if completed_task.is_terminal() && queue.is_empty() {
|
||||||
queue.push(Task::Idle {
|
queue.push(Task::Idle {
|
||||||
origin,
|
origin,
|
||||||
sigma_world: behaviour.idle.sigma_world,
|
sigma_world: behaviour.idle.sigma_world,
|
||||||
state: IdleState::Picking,
|
state: IdleState::Picking {
|
||||||
|
retry_after_tick: 0,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-34
@@ -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::constants::{ITILE_SIZE, TILE_SIZE};
|
||||||
use crate::entities::behaviour::IdleBehaviour;
|
use crate::entities::behaviour::IdleBehaviour;
|
||||||
use crate::entities::shared_components::Ambulatory;
|
use crate::entities::shared_components::Ambulatory;
|
||||||
@@ -26,8 +9,6 @@ use bevy::prelude::*;
|
|||||||
use bevy_rand::prelude::*;
|
use bevy_rand::prelude::*;
|
||||||
use rand::RngExt;
|
use rand::RngExt;
|
||||||
|
|
||||||
/// Execute one tick of Task::Idle.
|
|
||||||
/// Called by task_executor_system when current task is Idle.
|
|
||||||
pub(super) fn execute_idle(
|
pub(super) fn execute_idle(
|
||||||
task: &mut Task,
|
task: &mut Task,
|
||||||
transform: &Transform,
|
transform: &Transform,
|
||||||
@@ -49,7 +30,11 @@ pub(super) fn execute_idle(
|
|||||||
};
|
};
|
||||||
|
|
||||||
match state {
|
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) {
|
match pick_gaussian_target(origin, *sigma_world, tilemap, chunk_map, rng) {
|
||||||
Some(target) => {
|
Some(target) => {
|
||||||
ambulatory.target = Some(Vec3::new(
|
ambulatory.target = Some(Vec3::new(
|
||||||
@@ -62,8 +47,7 @@ pub(super) fn execute_idle(
|
|||||||
*state = IdleState::Moving { target };
|
*state = IdleState::Moving { target };
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
// No standable tile found yet (early startup, chunk loading).
|
*retry_after_tick = current_tick.saturating_add(30);
|
||||||
// Leave ambulatory.target as-is, try again next tick.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,9 +59,9 @@ pub(super) fn execute_idle(
|
|||||||
let arrive_threshold_sq = (TILE_SIZE * 1.5) * (TILE_SIZE * 1.5);
|
let arrive_threshold_sq = (TILE_SIZE * 1.5) * (TILE_SIZE * 1.5);
|
||||||
|
|
||||||
let arrived = dist_sq < arrive_threshold_sq;
|
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.target = None;
|
||||||
ambulatory.current_path = None;
|
ambulatory.current_path = None;
|
||||||
|
|
||||||
@@ -104,7 +88,9 @@ pub(super) fn execute_idle(
|
|||||||
next_flip_at,
|
next_flip_at,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
*state = IdleState::Picking;
|
*state = IdleState::Picking {
|
||||||
|
retry_after_tick: 0,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,7 +112,9 @@ pub(super) fn execute_idle(
|
|||||||
|
|
||||||
if *ticks_remaining == 0 {
|
if *ticks_remaining == 0 {
|
||||||
sprite.flip_x = false;
|
sprite.flip_x = false;
|
||||||
*state = IdleState::Picking;
|
*state = IdleState::Picking {
|
||||||
|
retry_after_tick: 0,
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
*ticks_remaining -= 1;
|
*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(
|
fn pick_gaussian_target(
|
||||||
origin: &IVec3,
|
origin: &IVec3,
|
||||||
sigma_world: f32,
|
sigma_world: f32,
|
||||||
@@ -194,9 +177,6 @@ fn pick_gaussian_target(
|
|||||||
chosen
|
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]
|
#[inline]
|
||||||
fn find_surface(world_x: i32, world_y: i32, tilemap: &TileMap) -> Option<IVec3> {
|
fn find_surface(world_x: i32, world_y: i32, tilemap: &TileMap) -> Option<IVec3> {
|
||||||
for z in -3i32..=4i32 {
|
for z in -3i32..=4i32 {
|
||||||
|
|||||||
+2
-3
@@ -5,7 +5,7 @@ use crate::entities::item::{
|
|||||||
initialize_item_rotation_state, item_tile_management_system, ItemRotationTimer,
|
initialize_item_rotation_state, item_tile_management_system, ItemRotationTimer,
|
||||||
};
|
};
|
||||||
use crate::game::SpawnDelay;
|
use crate::game::SpawnDelay;
|
||||||
use crate::plugins::{CargoPlugin, TasksPlugin, EntitiesPlugin};
|
use crate::plugins::{CargoPlugin, EntitiesPlugin, TasksPlugin};
|
||||||
use crate::world::WorldPlugin;
|
use crate::world::WorldPlugin;
|
||||||
|
|
||||||
mod camera;
|
mod camera;
|
||||||
@@ -34,7 +34,7 @@ fn main() {
|
|||||||
.set(WindowPlugin {
|
.set(WindowPlugin {
|
||||||
primary_window: Some(Window {
|
primary_window: Some(Window {
|
||||||
title: String::from("Dorf"),
|
title: String::from("Dorf"),
|
||||||
present_mode: bevy::window::PresentMode::Mailbox,
|
present_mode: bevy::window::PresentMode::AutoVsync,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -69,7 +69,6 @@ fn main() {
|
|||||||
entities::shared_systems::digging::dig_system,
|
entities::shared_systems::digging::dig_system,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
.insert_resource(ItemRotationTimer::default())
|
.insert_resource(ItemRotationTimer::default())
|
||||||
.add_systems(Update, initialize_item_rotation_state)
|
.add_systems(Update, initialize_item_rotation_state)
|
||||||
.add_systems(
|
.add_systems(
|
||||||
|
|||||||
Reference in New Issue
Block a user