use std::{ops::Range, time::Duration}; use bevy::prelude::*; use crate::{ colonist::{Colonist, MoveSpeed, Path}, jobs::{CurrentJob, Task}, pathfinding::find_path, rng::SimRng, sim::{SimPos, SimSystems, SimTick, TICKS_PER_HOUR, sim_pos}, world::{TILE_SIZE, TilePos, WorldMap}, worldgen::{WorldGenerated, spawn_tiles}, zones::Zones, }; pub struct AnimalsPlugin; impl Plugin for AnimalsPlugin { fn build(&self, app: &mut App) { app.add_systems(Startup, create_animal_assets) .add_systems(Update, populate.run_if(on_message::)) .add_systems( FixedUpdate, ( wander.in_set(SimSystems::Decide), (migrate, breed).in_set(SimSystems::Act), ), ); } } const WANDER_RADIUS: i32 = 10; pub const MAX_TAME: usize = 6; pub const PEN_RADIUS: i32 = 8; const BREED_CHANCE: f32 = 0.5; const BREED_INTERVAL_TICKS: u64 = 24 * TICKS_PER_HOUR; pub const FLEE_RANGE: i32 = 4; const FLEE_DISTANCE: f32 = 7.0; const IDLE_SECS: Range = 2.0..8.0; const MIN_DISTANCE_FROM_COLONY: f32 = 25.0; const MIGRATION_INTERVAL_TICKS: u64 = 2 * TICKS_PER_HOUR; const PLACEMENT_ATTEMPTS: usize = 200; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum AnimalKind { Deer, Rabbit, } impl AnimalKind { pub const ALL: [AnimalKind; 2] = [AnimalKind::Deer, AnimalKind::Rabbit]; pub fn meat(self) -> u32 { match self { AnimalKind::Deer => 30, AnimalKind::Rabbit => 8, } } pub fn population(self) -> usize { match self { AnimalKind::Deer => 10, AnimalKind::Rabbit => 16, } } pub fn tame_work(self) -> u32 { match self { AnimalKind::Deer => 600, AnimalKind::Rabbit => 300, } } pub fn hunt_work(self) -> u32 { match self { AnimalKind::Deer => 360, AnimalKind::Rabbit => 160, } } fn speed(self) -> f32 { match self { AnimalKind::Deer => 3.0, AnimalKind::Rabbit => 2.5, } } pub fn name(self) -> &'static str { match self { AnimalKind::Deer => "deer", AnimalKind::Rabbit => "rabbit", } } } #[derive(Component, Debug, Clone, Copy, PartialEq)] #[require(Transform, Visibility, TilePos, Path, SimPos, AnimalIdle)] pub struct Animal { pub kind: AnimalKind, } #[derive(Component, Debug, Clone, Copy, PartialEq, Eq)] pub struct Tame; #[derive(Component, Debug)] pub struct AnimalIdle(Timer); impl Default for AnimalIdle { fn default() -> Self { Self(Timer::from_seconds(IDLE_SECS.start, TimerMode::Once)) } } type Part = (Handle, Handle); #[derive(Resource, Default)] pub struct AnimalAssets { deer_body: Part, deer_head: Part, rabbit_body: Part, rabbit_ear: Part, } fn create_animal_assets( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, ) { let mut part = |mesh: Mesh, color: Color| (meshes.add(mesh), materials.add(color)); let deer = Color::srgb_u8(150, 100, 60); let rabbit = Color::srgb_u8(185, 175, 160); commands.insert_resource(AnimalAssets { deer_body: part( Ellipse::new(TILE_SIZE * 0.42, TILE_SIZE * 0.22).into(), deer, ), deer_head: part( Rectangle::new(TILE_SIZE * 0.2, TILE_SIZE * 0.3).into(), Color::srgb_u8(120, 80, 48), ), rabbit_body: part( Ellipse::new(TILE_SIZE * 0.22, TILE_SIZE * 0.15).into(), rabbit, ), rabbit_ear: part( Rectangle::new(TILE_SIZE * 0.06, TILE_SIZE * 0.2).into(), rabbit, ), }); } fn part_bundle((mesh, material): &Part, transform: Transform) -> impl Bundle { ( Mesh2d(mesh.clone()), MeshMaterial2d(material.clone()), transform, ) } pub fn spawn_animal( commands: &mut Commands, assets: &AnimalAssets, kind: AnimalKind, tile: UVec2, ) -> Entity { let at = |x: f32, y: f32| Transform::from_xyz(x * TILE_SIZE, y * TILE_SIZE, 0.01); let mut entity = commands.spawn(( Name::new(kind.name()), Animal { kind }, MoveSpeed(kind.speed()), TilePos(tile), sim_pos(WorldMap::tile_center(tile)), Transform::from_translation(WorldMap::tile_center(tile).extend(0.9)), )); entity.with_children(|c| match kind { AnimalKind::Deer => { c.spawn(part_bundle(&assets.deer_body, at(0.0, 0.0))); c.spawn(part_bundle(&assets.deer_head, at(0.38, 0.15))); } AnimalKind::Rabbit => { c.spawn(part_bundle(&assets.rabbit_body, at(0.0, 0.0))); c.spawn(part_bundle(&assets.rabbit_ear, at(0.1, 0.2))); c.spawn(part_bundle(&assets.rabbit_ear, at(0.18, 0.2))); } }); entity.id() } pub fn wild_tile(map: &WorldMap, rng: &mut SimRng, away_from: UVec2) -> Option { (0..PLACEMENT_ATTEMPTS).find_map(|_| { let tile = IVec2::new( rng.range_i32(0..map.size.x as i32), rng.range_i32(0..map.size.y as i32), ); let far = tile.as_vec2().distance(away_from.as_vec2()) >= MIN_DISTANCE_FROM_COLONY; let open = map.move_cost(tile).is_some() && map.plant(tile).is_none(); (far && open && !map.get(tile).is_some_and(|t| t.is_water())).then(|| tile.as_uvec2()) }) } fn populate( mut commands: Commands, map: Res, assets: Res, mut rng: ResMut, existing: Query>, ) { for entity in &existing { commands.entity(entity).despawn(); } let colony = spawn_tiles(&map, 1) .first() .copied() .unwrap_or(map.size / 2); for kind in AnimalKind::ALL { for _ in 0..kind.population() { if let Some(tile) = wild_tile(&map, &mut rng, colony) { spawn_animal(&mut commands, &assets, kind, tile); } } } } pub fn missing_animals(kind: AnimalKind, alive: usize) -> bool { alive < kind.population() } fn breed( mut commands: Commands, tick: Res, assets: Res, mut rng: ResMut, tame: Query<(&Animal, &TilePos), With>, ) { if !tick.0.is_multiple_of(BREED_INTERVAL_TICKS) || tick.0 == 0 { return; } let mut total = tame.iter().count(); for kind in AnimalKind::ALL { let herd: Vec = tame .iter() .filter(|(a, _)| a.kind == kind) .map(|(_, pos)| pos.0) .collect(); if herd.len() >= 2 && total < MAX_TAME && rng.range_f32(0.0..1.0) < BREED_CHANCE { let young = spawn_animal(&mut commands, &assets, kind, herd[0]); commands.entity(young).insert(Tame); total += 1; } } } fn migrate( mut commands: Commands, tick: Res, map: Res, assets: Res, mut rng: ResMut, animals: Query<&Animal, Without>, ) { if !tick.0.is_multiple_of(MIGRATION_INTERVAL_TICKS) || tick.0 == 0 { return; } let colony = spawn_tiles(&map, 1) .first() .copied() .unwrap_or(map.size / 2); for kind in AnimalKind::ALL { let alive = animals.iter().filter(|a| a.kind == kind).count(); if missing_animals(kind, alive) && let Some(tile) = wild_tile(&map, &mut rng, colony) { spawn_animal(&mut commands, &assets, kind, tile); } } } pub fn flee_goal(tile: UVec2, threats: &[UVec2]) -> Option { let near: Vec = threats .iter() .map(|t| t.as_ivec2()) .filter(|t| (*t - tile.as_ivec2()).abs().max_element() <= FLEE_RANGE) .collect(); if near.is_empty() { return None; } let away: Vec2 = near .iter() .map(|t| (tile.as_ivec2() - *t).as_vec2()) .sum::() .normalize_or(Vec2::X); Some(tile.as_ivec2() + (away * FLEE_DISTANCE).round().as_ivec2()) } #[allow(clippy::type_complexity)] pub fn wander( time: Res