src/animals.rs 9.4 K · 325 lines · raw · history

1 use std::{ops::Range, time::Duration};
2
3 use bevy::prelude::*;
4
5 use crate::{
6 colonist::{MoveSpeed, Path},
7 jobs::{Reservations, Target},
8 pathfinding::find_path,
9 rng::SimRng,
10 sim::{SimPos, SimSystems, SimTick, TICKS_PER_HOUR, sim_pos},
11 world::{TILE_SIZE, TilePos, WorldMap},
12 worldgen::{WorldGenerated, spawn_tiles},
13 };
14
15 pub struct AnimalsPlugin;
16
17 impl Plugin for AnimalsPlugin {
18 fn build(&self, app: &mut App) {
19 app.add_systems(Startup, create_animal_assets)
20 .add_systems(Update, populate.run_if(on_message::<WorldGenerated>))
21 .add_systems(
22 FixedUpdate,
23 (
24 wander.in_set(SimSystems::Decide),
25 migrate.in_set(SimSystems::Act),
26 ),
27 );
28 }
29 }
30
31 const WANDER_RADIUS: i32 = 10;
32 const IDLE_SECS: Range<f32> = 2.0..8.0;
33 const MIN_DISTANCE_FROM_COLONY: f32 = 25.0;
34 const MIGRATION_INTERVAL_TICKS: u64 = 2 * TICKS_PER_HOUR;
35 const PLACEMENT_ATTEMPTS: usize = 200;
36
37 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
38 pub enum AnimalKind {
39 Deer,
40 Rabbit,
41 }
42
43 impl AnimalKind {
44 pub const ALL: [AnimalKind; 2] = [AnimalKind::Deer, AnimalKind::Rabbit];
45
46 pub fn meat(self) -> u32 {
47 match self {
48 AnimalKind::Deer => 30,
49 AnimalKind::Rabbit => 8,
50 }
51 }
52
53 pub fn population(self) -> usize {
54 match self {
55 AnimalKind::Deer => 10,
56 AnimalKind::Rabbit => 16,
57 }
58 }
59
60 pub fn hunt_work(self) -> u32 {
61 match self {
62 AnimalKind::Deer => 360,
63 AnimalKind::Rabbit => 160,
64 }
65 }
66
67 fn speed(self) -> f32 {
68 match self {
69 AnimalKind::Deer => 3.0,
70 AnimalKind::Rabbit => 2.5,
71 }
72 }
73
74 pub fn name(self) -> &'static str {
75 match self {
76 AnimalKind::Deer => "deer",
77 AnimalKind::Rabbit => "rabbit",
78 }
79 }
80 }
81
82 #[derive(Component, Debug, Clone, Copy, PartialEq)]
83 #[require(Transform, Visibility, TilePos, Path, SimPos, AnimalIdle)]
84 pub struct Animal {
85 pub kind: AnimalKind,
86 }
87
88 #[derive(Component, Debug)]
89 pub struct AnimalIdle(Timer);
90
91 impl Default for AnimalIdle {
92 fn default() -> Self {
93 Self(Timer::from_seconds(IDLE_SECS.start, TimerMode::Once))
94 }
95 }
96
97 type Part = (Handle<Mesh>, Handle<ColorMaterial>);
98
99 #[derive(Resource, Default)]
100 pub struct AnimalAssets {
101 deer_body: Part,
102 deer_head: Part,
103 rabbit_body: Part,
104 rabbit_ear: Part,
105 }
106
107 fn create_animal_assets(
108 mut commands: Commands,
109 mut meshes: ResMut<Assets<Mesh>>,
110 mut materials: ResMut<Assets<ColorMaterial>>,
111 ) {
112 let mut part = |mesh: Mesh, color: Color| (meshes.add(mesh), materials.add(color));
113 let deer = Color::srgb_u8(150, 100, 60);
114 let rabbit = Color::srgb_u8(185, 175, 160);
115 commands.insert_resource(AnimalAssets {
116 deer_body: part(
117 Ellipse::new(TILE_SIZE * 0.42, TILE_SIZE * 0.22).into(),
118 deer,
119 ),
120 deer_head: part(
121 Rectangle::new(TILE_SIZE * 0.2, TILE_SIZE * 0.3).into(),
122 Color::srgb_u8(120, 80, 48),
123 ),
124 rabbit_body: part(
125 Ellipse::new(TILE_SIZE * 0.22, TILE_SIZE * 0.15).into(),
126 rabbit,
127 ),
128 rabbit_ear: part(
129 Rectangle::new(TILE_SIZE * 0.06, TILE_SIZE * 0.2).into(),
130 rabbit,
131 ),
132 });
133 }
134
135 fn part_bundle((mesh, material): &Part, transform: Transform) -> impl Bundle {
136 (
137 Mesh2d(mesh.clone()),
138 MeshMaterial2d(material.clone()),
139 transform,
140 )
141 }
142
143 pub fn spawn_animal(commands: &mut Commands, assets: &AnimalAssets, kind: AnimalKind, tile: UVec2) {
144 let at = |x: f32, y: f32| Transform::from_xyz(x * TILE_SIZE, y * TILE_SIZE, 0.01);
145 let mut entity = commands.spawn((
146 Name::new(kind.name()),
147 Animal { kind },
148 MoveSpeed(kind.speed()),
149 TilePos(tile),
150 sim_pos(WorldMap::tile_center(tile)),
151 Transform::from_translation(WorldMap::tile_center(tile).extend(0.9)),
152 ));
153 entity.with_children(|c| match kind {
154 AnimalKind::Deer => {
155 c.spawn(part_bundle(&assets.deer_body, at(0.0, 0.0)));
156 c.spawn(part_bundle(&assets.deer_head, at(0.38, 0.15)));
157 }
158 AnimalKind::Rabbit => {
159 c.spawn(part_bundle(&assets.rabbit_body, at(0.0, 0.0)));
160 c.spawn(part_bundle(&assets.rabbit_ear, at(0.1, 0.2)));
161 c.spawn(part_bundle(&assets.rabbit_ear, at(0.18, 0.2)));
162 }
163 });
164 }
165
166 pub fn wild_tile(map: &WorldMap, rng: &mut SimRng, away_from: UVec2) -> Option<UVec2> {
167 (0..PLACEMENT_ATTEMPTS).find_map(|_| {
168 let tile = IVec2::new(
169 rng.range_i32(0..map.size.x as i32),
170 rng.range_i32(0..map.size.y as i32),
171 );
172 let far = tile.as_vec2().distance(away_from.as_vec2()) >= MIN_DISTANCE_FROM_COLONY;
173 let open = map.move_cost(tile).is_some() && map.plant(tile).is_none();
174 (far && open && !map.get(tile).is_some_and(|t| t.is_water())).then(|| tile.as_uvec2())
175 })
176 }
177
178 fn populate(
179 mut commands: Commands,
180 map: Res<WorldMap>,
181 assets: Res<AnimalAssets>,
182 mut rng: ResMut<SimRng>,
183 existing: Query<Entity, With<Animal>>,
184 ) {
185 for entity in &existing {
186 commands.entity(entity).despawn();
187 }
188 let colony = spawn_tiles(&map, 1)
189 .first()
190 .copied()
191 .unwrap_or(map.size / 2);
192 for kind in AnimalKind::ALL {
193 for _ in 0..kind.population() {
194 if let Some(tile) = wild_tile(&map, &mut rng, colony) {
195 spawn_animal(&mut commands, &assets, kind, tile);
196 }
197 }
198 }
199 }
200
201 pub fn missing_animals(kind: AnimalKind, alive: usize) -> bool {
202 alive < kind.population()
203 }
204
205 fn migrate(
206 mut commands: Commands,
207 tick: Res<SimTick>,
208 map: Res<WorldMap>,
209 assets: Res<AnimalAssets>,
210 mut rng: ResMut<SimRng>,
211 animals: Query<&Animal>,
212 ) {
213 if !tick.0.is_multiple_of(MIGRATION_INTERVAL_TICKS) || tick.0 == 0 {
214 return;
215 }
216 let colony = spawn_tiles(&map, 1)
217 .first()
218 .copied()
219 .unwrap_or(map.size / 2);
220 for kind in AnimalKind::ALL {
221 let alive = animals.iter().filter(|a| a.kind == kind).count();
222 if missing_animals(kind, alive)
223 && let Some(tile) = wild_tile(&map, &mut rng, colony)
224 {
225 spawn_animal(&mut commands, &assets, kind, tile);
226 }
227 }
228 }
229
230 pub fn wander(
231 time: Res<Time>,
232 map: Res<WorldMap>,
233 reservations: Res<Reservations>,
234 mut rng: ResMut<SimRng>,
235 mut animals: Query<(Entity, &TilePos, &mut Path, &mut AnimalIdle), With<Animal>>,
236 ) {
237 for (entity, tile, mut path, mut idle) in &mut animals {
238 if reservations.holder(Target::Animal(entity)).is_some() {
239 path.0.truncate(1);
240 continue;
241 }
242 if !path.0.is_empty() || !idle.0.tick(time.delta()).is_finished() {
243 continue;
244 }
245 let goal = tile.0.as_ivec2()
246 + IVec2::new(
247 rng.range_i32(-WANDER_RADIUS..WANDER_RADIUS + 1),
248 rng.range_i32(-WANDER_RADIUS..WANDER_RADIUS + 1),
249 );
250 if map.in_bounds(goal)
251 && let Some(route) = find_path(&map, tile.0, goal.as_uvec2())
252 {
253 path.0 = route.into();
254 let secs = rng.range_f32(IDLE_SECS);
255 idle.0.set_duration(Duration::from_secs_f32(secs));
256 idle.0.reset();
257 }
258 }
259 }
260
261 #[cfg(test)]
262 mod tests {
263 use super::*;
264 use crate::world::Terrain;
265
266 #[test]
267 fn wild_animals_appear_away_from_the_colony_on_open_ground() {
268 let mut map = WorldMap::filled(UVec2::splat(80), Terrain::Grass);
269 map.set_plant(UVec2::new(70, 70), Some(crate::world::Plant::Tree));
270 let mut rng = SimRng::new(3);
271 for _ in 0..50 {
272 let tile = wild_tile(&map, &mut rng, UVec2::new(40, 40)).unwrap();
273 assert!(tile.as_vec2().distance(Vec2::splat(40.0)) >= MIN_DISTANCE_FROM_COLONY);
274 assert!(map.plant(tile.as_ivec2()).is_none());
275 }
276 }
277
278 #[test]
279 fn herds_regrow_up_to_their_population() {
280 assert!(missing_animals(AnimalKind::Deer, 3));
281 assert!(!missing_animals(
282 AnimalKind::Deer,
283 AnimalKind::Deer.population()
284 ));
285 }
286
287 #[test]
288 fn idle_animals_wander_and_hunted_ones_stand_still() {
289 let mut app = App::new();
290 app.insert_resource(WorldMap::filled(UVec2::splat(30), Terrain::Grass))
291 .insert_resource(SimRng::new(1))
292 .init_resource::<Time>()
293 .init_resource::<Reservations>()
294 .add_systems(Update, wander);
295 let free = app
296 .world_mut()
297 .spawn((
298 Animal {
299 kind: AnimalKind::Deer,
300 },
301 TilePos(UVec2::splat(15)),
302 ))
303 .id();
304 let hunted = app
305 .world_mut()
306 .spawn((
307 Animal {
308 kind: AnimalKind::Rabbit,
309 },
310 TilePos(UVec2::splat(10)),
311 ))
312 .id();
313 app.world_mut()
314 .resource_mut::<Reservations>()
315 .reserve(Target::Animal(hunted), Entity::PLACEHOLDER);
316 for _ in 0..10 {
317 app.world_mut()
318 .resource_mut::<Time>()
319 .advance_by(Duration::from_secs(1));
320 app.update();
321 }
322 assert!(!app.world().get::<Path>(free).unwrap().0.is_empty());
323 assert!(app.world().get::<Path>(hunted).unwrap().0.is_empty());
324 }
325 }