src/animals.rs 13.3 K · 443 lines · raw · history

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