| @@ -3,8 +3,7 @@ use std::{ops::Range, time::Duration}; |
| 3 |
3 |
use bevy::prelude::*; |
| 4 |
4 |
|
| 5 |
5 |
use crate::{ |
| 6 |
|
- colonist::{MoveSpeed, Path}, |
| 7 |
|
- jobs::{Reservations, Target}, |
|
6 |
+ colonist::{Colonist, MoveSpeed, Path}, |
| 8 |
7 |
pathfinding::find_path, |
| 9 |
8 |
rng::SimRng, |
| 10 |
9 |
sim::{SimPos, SimSystems, SimTick, TICKS_PER_HOUR, sim_pos}, |
| @@ -29,6 +28,8 @@ impl Plugin for AnimalsPlugin { |
| 29 |
28 |
} |
| 30 |
29 |
|
| 31 |
30 |
const WANDER_RADIUS: i32 = 10; |
|
31 |
+pub const FLEE_RANGE: i32 = 4; |
|
32 |
+const FLEE_DISTANCE: f32 = 7.0; |
| 32 |
33 |
const IDLE_SECS: Range<f32> = 2.0..8.0; |
| 33 |
34 |
const MIN_DISTANCE_FROM_COLONY: f32 = 25.0; |
| 34 |
35 |
const MIGRATION_INTERVAL_TICKS: u64 = 2 * TICKS_PER_HOUR; |
| @@ -227,17 +228,42 @@ fn migrate( |
| 227 |
228 |
} |
| 228 |
229 |
} |
| 229 |
230 |
|
|
231 |
+pub fn flee_goal(tile: UVec2, threats: &[UVec2]) -> Option<IVec2> { |
|
232 |
+ let near: Vec<IVec2> = threats |
|
233 |
+ .iter() |
|
234 |
+ .map(|t| t.as_ivec2()) |
|
235 |
+ .filter(|t| (*t - tile.as_ivec2()).abs().max_element() <= FLEE_RANGE) |
|
236 |
+ .collect(); |
|
237 |
+ if near.is_empty() { |
|
238 |
+ return None; |
|
239 |
+ } |
|
240 |
+ let away: Vec2 = near |
|
241 |
+ .iter() |
|
242 |
+ .map(|t| (tile.as_ivec2() - *t).as_vec2()) |
|
243 |
+ .sum::<Vec2>() |
|
244 |
+ .normalize_or(Vec2::X); |
|
245 |
+ Some(tile.as_ivec2() + (away * FLEE_DISTANCE).round().as_ivec2()) |
|
246 |
+} |
|
247 |
+ |
| 230 |
248 |
pub fn wander( |
| 231 |
249 |
time: Res<Time>, |
| 232 |
250 |
map: Res<WorldMap>, |
| 233 |
|
- reservations: Res<Reservations>, |
| 234 |
251 |
mut rng: ResMut<SimRng>, |
| 235 |
|
- mut animals: Query<(Entity, &TilePos, &mut Path, &mut AnimalIdle), With<Animal>>, |
|
252 |
+ colonists: Query<&TilePos, (With<Colonist>, Without<Animal>)>, |
|
253 |
+ mut animals: Query<(&TilePos, &mut Path, &mut AnimalIdle), With<Animal>>, |
| 236 |
254 |
) { |
| 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; |
|
255 |
+ let threats: Vec<UVec2> = colonists.iter().map(|t| t.0).collect(); |
|
256 |
+ for (tile, mut path, mut idle) in &mut animals { |
|
257 |
+ let heading_into_danger = path |
|
258 |
+ .0 |
|
259 |
+ .back() |
|
260 |
+ .is_none_or(|end| flee_goal(*end, &threats).is_some()); |
|
261 |
+ if heading_into_danger && let Some(goal) = flee_goal(tile.0, &threats) { |
|
262 |
+ let goal = goal.clamp(IVec2::ZERO, map.size.as_ivec2() - 1).as_uvec2(); |
|
263 |
+ if let Some(route) = find_path(&map, tile.0, goal) { |
|
264 |
+ path.0 = route.into(); |
|
265 |
+ continue; |
|
266 |
+ } |
| 241 |
267 |
} |
| 242 |
268 |
if !path.0.is_empty() || !idle.0.tick(time.delta()).is_finished() { |
| 243 |
269 |
continue; |
| @@ -264,6 +290,17 @@ mod tests { |
| 264 |
290 |
use crate::world::Terrain; |
| 265 |
291 |
|
| 266 |
292 |
#[test] |
|
293 |
+ fn animals_run_away_from_nearby_colonists() { |
|
294 |
+ let tile = UVec2::new(10, 10); |
|
295 |
+ let goal = flee_goal(tile, &[UVec2::new(8, 10), UVec2::new(30, 30)]).unwrap(); |
|
296 |
+ assert!(goal.x > 10 + FLEE_RANGE); |
|
297 |
+ assert_eq!( |
|
298 |
+ flee_goal(tile, &[UVec2::new(10, 10 + FLEE_RANGE as u32 + 1)]), |
|
299 |
+ None |
|
300 |
+ ); |
|
301 |
+ } |
|
302 |
+ |
|
303 |
+ #[test] |
| 267 |
304 |
fn wild_animals_appear_away_from_the_colony_on_open_ground() { |
| 268 |
305 |
let mut map = WorldMap::filled(UVec2::splat(80), Terrain::Grass); |
| 269 |
306 |
map.set_plant(UVec2::new(70, 70), Some(crate::world::Plant::Tree)); |
| @@ -284,42 +321,42 @@ mod tests { |
| 284 |
321 |
)); |
| 285 |
322 |
} |
| 286 |
323 |
|
|
324 |
+ |
|
325 |
+ |
| 287 |
326 |
#[test] |
| 288 |
|
- fn idle_animals_wander_and_hunted_ones_stand_still() { |
|
327 |
+ fn idle_animals_wander_and_startled_ones_bolt() { |
| 289 |
328 |
let mut app = App::new(); |
| 290 |
329 |
app.insert_resource(WorldMap::filled(UVec2::splat(30), Terrain::Grass)) |
| 291 |
330 |
.insert_resource(SimRng::new(1)) |
| 292 |
331 |
.init_resource::<Time>() |
| 293 |
|
- .init_resource::<Reservations>() |
| 294 |
332 |
.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(); |
|
333 |
+ let spawn = |app: &mut App, tile: UVec2| { |
|
334 |
+ app.world_mut() |
|
335 |
+ .spawn(( |
|
336 |
+ Animal { |
|
337 |
+ kind: AnimalKind::Deer, |
|
338 |
+ }, |
|
339 |
+ TilePos(tile), |
|
340 |
+ )) |
|
341 |
+ .id() |
|
342 |
+ }; |
|
343 |
+ let idle = spawn(&mut app, UVec2::splat(25)); |
|
344 |
+ let startled = spawn(&mut app, UVec2::splat(10)); |
| 313 |
345 |
app.world_mut() |
| 314 |
|
- .resource_mut::<Reservations>() |
| 315 |
|
- .reserve(Target::Animal(hunted), Entity::PLACEHOLDER); |
|
346 |
+ .spawn((Colonist { name: "Ada".into() }, TilePos(UVec2::new(9, 10)))); |
|
347 |
+ app.update(); |
|
348 |
+ let route = &app.world().get::<Path>(startled).unwrap().0; |
|
349 |
+ assert!( |
|
350 |
+ route |
|
351 |
+ .back() |
|
352 |
+ .is_some_and(|end| end.x > 10 + FLEE_RANGE as u32) |
|
353 |
+ ); |
| 316 |
354 |
for _ in 0..10 { |
| 317 |
355 |
app.world_mut() |
| 318 |
356 |
.resource_mut::<Time>() |
| 319 |
357 |
.advance_by(Duration::from_secs(1)); |
| 320 |
358 |
app.update(); |
| 321 |
359 |
} |
| 322 |
|
- assert!(!app.world().get::<Path>(free).unwrap().0.is_empty()); |
| 323 |
|
- assert!(app.world().get::<Path>(hunted).unwrap().0.is_empty()); |
|
360 |
+ assert!(!app.world().get::<Path>(idle).unwrap().0.is_empty()); |
| 324 |
361 |
} |
| 325 |
362 |
} |