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::job_queue::JobId; use crate::entities::tasks::{IdleState, Task, TaskQueue, TaskState}; use crate::game::SpawnDelay; use crate::world::VisibleGameEntity; use bevy::prelude::*; use bevy_rand::prelude::*; use rand::{RngExt, SeedableRng}; use std::collections::VecDeque; #[derive(Bundle)] pub struct Dorf { ambulatory: Ambulatory, sprite: Sprite, transform: Transform, visibility: Visibility, haul_slot: HaulSlot, carry_visual: CarryVisualState, task_queue: TaskQueue, task_state: TaskState, entity_type: EntityType, } impl Dorf { pub fn new(asset_server: &Res, position: Vec3) -> Self { let walk_speed = 5.; let run_speed = 6.; 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 { walk_speed, base_walk_speed: walk_speed, run_speed, target: None, current_path: None, path_index: 0, step_recovery: 0, validation_cooldown: 0, move_direction: Vec2::ZERO, step_history: [0i16; 4], }, sprite: Sprite { image: normal_sprite.clone(), ..Default::default() }, transform: Transform::from_translation(position).with_scale(Vec3::splat(PIXEL_RATIO)), visibility: Visibility::Hidden, haul_slot: HaulSlot::default(), carry_visual: CarryVisualState::new(normal_sprite, carry_sprite), task_queue: TaskQueue { tasks: VecDeque::from([Task::Idle { job_id: JobId::default(), origin, sigma_world: behaviour.idle.sigma_world, state: IdleState::Picking { retry_after_tick: 0, retry_count: 0, }, }]), }, task_state: TaskState::Pending, entity_type: EntityType("dorf"), } } } pub fn spawn_dorfs( mut commands: Commands, asset_server: Res, config: Res, mut delay: ResMut, mut has_spawned: Local, ) { if *has_spawned { return; } if delay.0 < 60 { delay.0 += 1; return; } *has_spawned = true; // Create deterministic RNG from SEED - same pattern as forestry.rs use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; let mut hasher = DefaultHasher::new(); crate::constants::SEED.hash(&mut hasher); let seed = hasher.finish(); let mut rng = WyRand::seed_from_u64(seed); use crate::world::chunks::Z_ABOVE; for _ in 0..config.spawn_counts.dorfs { let raw_x = rng.random_range(-512.0f32..512.0f32); let raw_y = rng.random_range(-512.0f32..512.0f32); let grid_x = (raw_x / TILE_SIZE).round() * TILE_SIZE; let grid_y = (raw_y / TILE_SIZE).round() * TILE_SIZE; // Spawn at top of valid world range (Z_ABOVE * TILE_SIZE) // The movement system will snap/fall them to surface level let grid_z = Z_ABOVE * TILE_SIZE; info!( "[SPAWN] Dorf spawning at ({}, {}, {}) z_level={}", grid_x, grid_y, grid_z, grid_z / TILE_SIZE ); let cit = commands .spawn(Dorf::new(&asset_server, Vec3::new(grid_x, grid_y, grid_z))) .id(); commands.entity(cit).insert(VisibleGameEntity); } }