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