src/animals.rs 15.3 K · 510 lines · raw · history

1 use serde::{Deserialize, Serialize};
2 use std::{ops::Range, time::Duration};
3
4 use bevy::prelude::*;
5
6 use crate::{
7 colonist::{Colonist, MoveSpeed, Path},
8 jobs::{CurrentJob, Task},
9 pathfinding::find_path,
10 rng::SimRng,
11 sim::{SimPos, SimSystems, SimTick, TICKS_PER_HOUR, sim_pos},
12 threats::{Health, Predator},
13 world::{TILE_SIZE, TilePos, WorldMap},
14 worldgen::{WorldGenerated, spawn_tiles},
15 zones::Zones,
16 };
17
18 pub struct AnimalsPlugin;
19
20 impl Plugin for AnimalsPlugin {
21 fn build(&self, app: &mut App) {
22 app.add_systems(Startup, create_animal_assets)
23 .add_systems(Update, populate.run_if(on_message::<WorldGenerated>))
24 .add_systems(
25 FixedUpdate,
26 (
27 wander.in_set(SimSystems::Decide),
28 (migrate, breed).in_set(SimSystems::Act),
29 ),
30 );
31 }
32 }
33
34 const WANDER_RADIUS: i32 = 10;
35 pub const MAX_TAME: usize = 6;
36 pub const PEN_RADIUS: i32 = 8;
37 const BREED_CHANCE: f32 = 0.5;
38 const BREED_INTERVAL_TICKS: u64 = 24 * TICKS_PER_HOUR;
39 pub const FLEE_RANGE: i32 = 4;
40 const FLEE_DISTANCE: f32 = 7.0;
41 const IDLE_SECS: Range<f32> = 2.0..8.0;
42 const MIN_DISTANCE_FROM_COLONY: f32 = 25.0;
43 const MIGRATION_INTERVAL_TICKS: u64 = 2 * TICKS_PER_HOUR;
44 const PLACEMENT_ATTEMPTS: usize = 200;
45
46 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
47 pub enum AnimalKind {
48 Deer,
49 Rabbit,
50 Wolf,
51 }
52
53 impl AnimalKind {
54 pub const ALL: [AnimalKind; 3] = [AnimalKind::Deer, AnimalKind::Rabbit, AnimalKind::Wolf];
55
56 pub fn meat(self) -> u32 {
57 match self {
58 AnimalKind::Deer => 30,
59 AnimalKind::Rabbit => 8,
60 AnimalKind::Wolf => 12,
61 }
62 }
63
64 pub fn population(self) -> usize {
65 match self {
66 AnimalKind::Deer => 10,
67 AnimalKind::Rabbit => 16,
68 AnimalKind::Wolf => 3,
69 }
70 }
71
72 pub fn migration_ticks(self) -> u64 {
73 match self {
74 AnimalKind::Wolf => 24 * TICKS_PER_HOUR,
75 _ => MIGRATION_INTERVAL_TICKS,
76 }
77 }
78
79 pub fn is_predator(self) -> bool {
80 self == AnimalKind::Wolf
81 }
82
83 pub fn tame_work(self) -> u32 {
84 match self {
85 AnimalKind::Deer => 600,
86 AnimalKind::Rabbit => 300,
87 AnimalKind::Wolf => u32::MAX,
88 }
89 }
90
91 pub fn hunt_work(self) -> u32 {
92 match self {
93 AnimalKind::Deer => 360,
94 AnimalKind::Rabbit => 160,
95 AnimalKind::Wolf => 300,
96 }
97 }
98
99 fn speed(self) -> f32 {
100 match self {
101 AnimalKind::Deer => 3.0,
102 AnimalKind::Rabbit => 2.5,
103 AnimalKind::Wolf => 3.4,
104 }
105 }
106
107 pub fn name(self) -> &'static str {
108 match self {
109 AnimalKind::Deer => "deer",
110 AnimalKind::Rabbit => "rabbit",
111 AnimalKind::Wolf => "wolf",
112 }
113 }
114 }
115
116 #[derive(Component, Debug, Clone, Copy, PartialEq)]
117 #[require(Transform, Visibility, TilePos, Path, SimPos, AnimalIdle, Health)]
118 pub struct Animal {
119 pub kind: AnimalKind,
120 }
121
122 #[derive(Component, Debug, Clone, Copy, PartialEq, Eq)]
123 pub struct Tame;
124
125 #[derive(Component, Debug)]
126 pub struct AnimalIdle(Timer);
127
128 impl Default for AnimalIdle {
129 fn default() -> Self {
130 Self(Timer::from_seconds(IDLE_SECS.start, TimerMode::Once))
131 }
132 }
133
134 type Part = (Handle<Mesh>, Handle<ColorMaterial>);
135
136 #[derive(Resource, Default)]
137 pub struct AnimalAssets {
138 deer_body: Part,
139 deer_head: Part,
140 rabbit_body: Part,
141 rabbit_ear: Part,
142 wolf_body: Part,
143 wolf_head: Part,
144 wolf_tail: Part,
145 }
146
147 fn create_animal_assets(
148 mut commands: Commands,
149 mut meshes: ResMut<Assets<Mesh>>,
150 mut materials: ResMut<Assets<ColorMaterial>>,
151 ) {
152 let mut part = |mesh: Mesh, color: Color| (meshes.add(mesh), materials.add(color));
153 let deer = Color::srgb_u8(150, 100, 60);
154 let rabbit = Color::srgb_u8(185, 175, 160);
155 commands.insert_resource(AnimalAssets {
156 deer_body: part(
157 Ellipse::new(TILE_SIZE * 0.42, TILE_SIZE * 0.22).into(),
158 deer,
159 ),
160 deer_head: part(
161 Rectangle::new(TILE_SIZE * 0.2, TILE_SIZE * 0.3).into(),
162 Color::srgb_u8(120, 80, 48),
163 ),
164 rabbit_body: part(
165 Ellipse::new(TILE_SIZE * 0.22, TILE_SIZE * 0.15).into(),
166 rabbit,
167 ),
168 rabbit_ear: part(
169 Rectangle::new(TILE_SIZE * 0.06, TILE_SIZE * 0.2).into(),
170 rabbit,
171 ),
172 wolf_body: part(
173 Ellipse::new(TILE_SIZE * 0.38, TILE_SIZE * 0.17).into(),
174 Color::srgb_u8(95, 95, 102),
175 ),
176 wolf_head: part(
177 Triangle2d::new(
178 Vec2::new(0.0, -TILE_SIZE * 0.13),
179 Vec2::new(0.0, TILE_SIZE * 0.13),
180 Vec2::new(TILE_SIZE * 0.28, 0.0),
181 )
182 .into(),
183 Color::srgb_u8(60, 60, 66),
184 ),
185 wolf_tail: part(
186 Rectangle::new(TILE_SIZE * 0.22, TILE_SIZE * 0.07).into(),
187 Color::srgb_u8(70, 70, 76),
188 ),
189 });
190 }
191
192 fn part_bundle((mesh, material): &Part, transform: Transform) -> impl Bundle {
193 (
194 Mesh2d(mesh.clone()),
195 MeshMaterial2d(material.clone()),
196 transform,
197 )
198 }
199
200 pub fn spawn_animal(
201 commands: &mut Commands,
202 assets: &AnimalAssets,
203 kind: AnimalKind,
204 tile: UVec2,
205 ) -> Entity {
206 let at = |x: f32, y: f32| Transform::from_xyz(x * TILE_SIZE, y * TILE_SIZE, 0.01);
207 let mut entity = commands.spawn((
208 Name::new(kind.name()),
209 Animal { kind },
210 MoveSpeed(kind.speed()),
211 TilePos(tile),
212 sim_pos(WorldMap::tile_center(tile)),
213 Transform::from_translation(WorldMap::tile_center(tile).extend(0.9)),
214 ));
215 entity.with_children(|c| match kind {
216 AnimalKind::Deer => {
217 c.spawn(part_bundle(&assets.deer_body, at(0.0, 0.0)));
218 c.spawn(part_bundle(&assets.deer_head, at(0.38, 0.15)));
219 }
220 AnimalKind::Rabbit => {
221 c.spawn(part_bundle(&assets.rabbit_body, at(0.0, 0.0)));
222 c.spawn(part_bundle(&assets.rabbit_ear, at(0.1, 0.2)));
223 c.spawn(part_bundle(&assets.rabbit_ear, at(0.18, 0.2)));
224 }
225 AnimalKind::Wolf => {
226 c.spawn(part_bundle(&assets.wolf_tail, at(-0.42, 0.06)));
227 c.spawn(part_bundle(&assets.wolf_body, at(0.0, 0.0)));
228 c.spawn(part_bundle(&assets.wolf_head, at(0.3, 0.05)));
229 }
230 });
231 if kind.is_predator() {
232 entity.insert(Predator::default());
233 }
234 entity.id()
235 }
236
237 pub fn wild_tile(map: &WorldMap, rng: &mut SimRng, away_from: UVec2) -> Option<UVec2> {
238 (0..PLACEMENT_ATTEMPTS).find_map(|_| {
239 let tile = IVec2::new(
240 rng.range_i32(0..map.size.x as i32),
241 rng.range_i32(0..map.size.y as i32),
242 );
243 let far = tile.as_vec2().distance(away_from.as_vec2()) >= MIN_DISTANCE_FROM_COLONY;
244 let open = map.move_cost(tile).is_some() && map.plant(tile).is_none();
245 (far && open && !map.get(tile).is_some_and(|t| t.is_water())).then(|| tile.as_uvec2())
246 })
247 }
248
249 fn populate(
250 mut commands: Commands,
251 map: Res<WorldMap>,
252 assets: Res<AnimalAssets>,
253 mut rng: ResMut<SimRng>,
254 existing: Query<Entity, With<Animal>>,
255 ) {
256 for entity in &existing {
257 commands.entity(entity).despawn();
258 }
259 let colony = spawn_tiles(&map, 1)
260 .first()
261 .copied()
262 .unwrap_or(map.size / 2);
263 for kind in AnimalKind::ALL {
264 for _ in 0..kind.population() {
265 if let Some(tile) = wild_tile(&map, &mut rng, colony) {
266 spawn_animal(&mut commands, &assets, kind, tile);
267 }
268 }
269 }
270 }
271
272 pub fn missing_animals(kind: AnimalKind, alive: usize) -> bool {
273 alive < kind.population()
274 }
275
276 fn breed(
277 mut commands: Commands,
278 tick: Res<SimTick>,
279 assets: Res<AnimalAssets>,
280 mut rng: ResMut<SimRng>,
281 tame: Query<(&Animal, &TilePos), With<Tame>>,
282 ) {
283 if !tick.0.is_multiple_of(BREED_INTERVAL_TICKS) || tick.0 == 0 {
284 return;
285 }
286 let mut total = tame.iter().count();
287 for kind in AnimalKind::ALL {
288 let herd: Vec<UVec2> = tame
289 .iter()
290 .filter(|(a, _)| a.kind == kind)
291 .map(|(_, pos)| pos.0)
292 .collect();
293 if herd.len() >= 2 && total < MAX_TAME && rng.range_f32(0.0..1.0) < BREED_CHANCE {
294 let young = spawn_animal(&mut commands, &assets, kind, herd[0]);
295 commands.entity(young).insert(Tame);
296 total += 1;
297 }
298 }
299 }
300
301 fn migrate(
302 mut commands: Commands,
303 tick: Res<SimTick>,
304 map: Res<WorldMap>,
305 assets: Res<AnimalAssets>,
306 mut rng: ResMut<SimRng>,
307 animals: Query<&Animal, Without<Tame>>,
308 zones: Res<Zones>,
309 ) {
310 if !tick.0.is_multiple_of(MIGRATION_INTERVAL_TICKS) || tick.0 == 0 {
311 return;
312 }
313 let colony = zones.home(map.size / 2);
314 for kind in AnimalKind::ALL {
315 if !tick.0.is_multiple_of(kind.migration_ticks()) {
316 continue;
317 }
318 let alive = animals.iter().filter(|a| a.kind == kind).count();
319 if missing_animals(kind, alive)
320 && let Some(tile) = wild_tile(&map, &mut rng, colony)
321 {
322 spawn_animal(&mut commands, &assets, kind, tile);
323 }
324 }
325 }
326
327 pub fn flee_goal(tile: UVec2, threats: &[UVec2]) -> Option<IVec2> {
328 let near: Vec<IVec2> = threats
329 .iter()
330 .map(|t| t.as_ivec2())
331 .filter(|t| (*t - tile.as_ivec2()).abs().max_element() <= FLEE_RANGE)
332 .collect();
333 if near.is_empty() {
334 return None;
335 }
336 let away: Vec2 = near
337 .iter()
338 .map(|t| (tile.as_ivec2() - *t).as_vec2())
339 .sum::<Vec2>()
340 .normalize_or(Vec2::X);
341 Some(tile.as_ivec2() + (away * FLEE_DISTANCE).round().as_ivec2())
342 }
343
344 #[allow(clippy::type_complexity)]
345 pub fn wander(
346 time: Res<Time>,
347 map: Res<WorldMap>,
348 zones: Res<Zones>,
349 mut rng: ResMut<SimRng>,
350 colonists: Query<(&TilePos, &CurrentJob), (With<Colonist>, Without<Animal>)>,
351 mut animals: Query<
352 (
353 Entity,
354 &TilePos,
355 &mut Path,
356 &mut AnimalIdle,
357 Has<Tame>,
358 Option<&Predator>,
359 ),
360 With<Animal>,
361 >,
362 ) {
363 let threats: Vec<UVec2> = colonists
364 .iter()
365 .map(|(t, _)| t.0)
366 .chain(animals.iter().filter(|a| a.5.is_some()).map(|a| a.1.0))
367 .collect();
368 let being_tamed: Vec<Entity> = colonists
369 .iter()
370 .filter_map(|(_, job)| match job.0?.task {
371 Task::Tame { animal, .. } => Some(animal),
372 _ => None,
373 })
374 .collect();
375 let home = zones.home(map.size / 2).as_ivec2();
376 for (entity, tile, mut path, mut idle, tame, predator) in &mut animals {
377 if predator.is_some_and(|p| p.target.is_some()) {
378 continue;
379 }
380 if being_tamed.contains(&entity) {
381 path.0.truncate(1);
382 continue;
383 }
384 if tame {
385 if !path.0.is_empty() || !idle.0.tick(time.delta()).is_finished() {
386 continue;
387 }
388 let goal = home
389 + IVec2::new(
390 rng.range_i32(-PEN_RADIUS..PEN_RADIUS + 1),
391 rng.range_i32(-PEN_RADIUS..PEN_RADIUS + 1),
392 );
393 if map.in_bounds(goal)
394 && let Some(route) = find_path(&map, tile.0, goal.as_uvec2())
395 {
396 path.0 = route.into();
397 idle.0
398 .set_duration(Duration::from_secs_f32(rng.range_f32(IDLE_SECS)));
399 idle.0.reset();
400 }
401 continue;
402 }
403 let threats: &[UVec2] = if predator.is_some() { &[] } else { &threats };
404 let heading_into_danger = path
405 .0
406 .back()
407 .is_none_or(|end| flee_goal(*end, threats).is_some());
408 if heading_into_danger && let Some(goal) = flee_goal(tile.0, threats) {
409 let goal = goal.clamp(IVec2::ZERO, map.size.as_ivec2() - 1).as_uvec2();
410 if let Some(route) = find_path(&map, tile.0, goal) {
411 path.0 = route.into();
412 continue;
413 }
414 }
415 if !path.0.is_empty() || !idle.0.tick(time.delta()).is_finished() {
416 continue;
417 }
418 let goal = tile.0.as_ivec2()
419 + IVec2::new(
420 rng.range_i32(-WANDER_RADIUS..WANDER_RADIUS + 1),
421 rng.range_i32(-WANDER_RADIUS..WANDER_RADIUS + 1),
422 );
423 if map.in_bounds(goal)
424 && let Some(route) = find_path(&map, tile.0, goal.as_uvec2())
425 {
426 path.0 = route.into();
427 let secs = rng.range_f32(IDLE_SECS);
428 idle.0.set_duration(Duration::from_secs_f32(secs));
429 idle.0.reset();
430 }
431 }
432 }
433
434 #[cfg(test)]
435 mod tests {
436 use super::*;
437 use crate::world::Terrain;
438
439 #[test]
440 fn animals_run_away_from_nearby_colonists() {
441 let tile = UVec2::new(10, 10);
442 let goal = flee_goal(tile, &[UVec2::new(8, 10), UVec2::new(30, 30)]).unwrap();
443 assert!(goal.x > 10 + FLEE_RANGE);
444 assert_eq!(
445 flee_goal(tile, &[UVec2::new(10, 10 + FLEE_RANGE as u32 + 1)]),
446 None
447 );
448 }
449
450 #[test]
451 fn wild_animals_appear_away_from_the_colony_on_open_ground() {
452 let mut map = WorldMap::filled(UVec2::splat(80), Terrain::Grass);
453 map.set_plant(UVec2::new(70, 70), Some(crate::world::Plant::Tree));
454 let mut rng = SimRng::new(3);
455 for _ in 0..50 {
456 let tile = wild_tile(&map, &mut rng, UVec2::new(40, 40)).unwrap();
457 assert!(tile.as_vec2().distance(Vec2::splat(40.0)) >= MIN_DISTANCE_FROM_COLONY);
458 assert!(map.plant(tile.as_ivec2()).is_none());
459 }
460 }
461
462 #[test]
463 fn herds_regrow_up_to_their_population() {
464 assert!(missing_animals(AnimalKind::Deer, 3));
465 assert!(!missing_animals(
466 AnimalKind::Deer,
467 AnimalKind::Deer.population()
468 ));
469 }
470
471 // Hunted animals used to stand still for the hunter; now everything wild bolts from people
472 // nearby, which is why hunters shoot from beyond FLEE_RANGE.
473 #[test]
474 fn idle_animals_wander_and_startled_ones_bolt() {
475 let mut app = App::new();
476 app.insert_resource(WorldMap::filled(UVec2::splat(30), Terrain::Grass))
477 .insert_resource(SimRng::new(1))
478 .init_resource::<Time>()
479 .init_resource::<Zones>()
480 .add_systems(Update, wander);
481 let spawn = |app: &mut App, tile: UVec2| {
482 app.world_mut()
483 .spawn((
484 Animal {
485 kind: AnimalKind::Deer,
486 },
487 TilePos(tile),
488 ))
489 .id()
490 };
491 let idle = spawn(&mut app, UVec2::splat(25));
492 let startled = spawn(&mut app, UVec2::splat(10));
493 app.world_mut()
494 .spawn((Colonist { name: "Ada".into() }, TilePos(UVec2::new(9, 10))));
495 app.update();
496 let route = &app.world().get::<Path>(startled).unwrap().0;
497 assert!(
498 route
499 .back()
500 .is_some_and(|end| end.x > 10 + FLEE_RANGE as u32)
501 );
502 for _ in 0..10 {
503 app.world_mut()
504 .resource_mut::<Time>()
505 .advance_by(Duration::from_secs(1));
506 app.update();
507 }
508 assert!(!app.world().get::<Path>(idle).unwrap().0.is_empty());
509 }
510 }