Hunters explore forests to hunt game

This commit is contained in:
Joshua Barretto 2023-04-02 23:48:05 +01:00
parent b402e450cf
commit 7175f7f02f
6 changed files with 318 additions and 254 deletions

View File

@ -214,6 +214,8 @@ pub enum NpcActivity {
/// (travel_to, speed_factor) /// (travel_to, speed_factor)
Goto(Vec3<f32>, f32), Goto(Vec3<f32>, f32),
Gather(&'static [ChunkResource]), Gather(&'static [ChunkResource]),
// TODO: Generalise to other entities? What kinds of animals?
HuntAnimals,
} }
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]

View File

@ -62,6 +62,8 @@ impl Controller {
self.activity = Some(NpcActivity::Gather(resources)); self.activity = Some(NpcActivity::Gather(resources));
} }
pub fn do_hunt_animals(&mut self) { self.activity = Some(NpcActivity::HuntAnimals); }
pub fn do_greet(&mut self, actor: Actor) { self.actions.push(NpcAction::Greet(actor)); } pub fn do_greet(&mut self, actor: Actor) { self.actions.push(NpcAction::Greet(actor)); }
} }

View File

@ -528,6 +528,25 @@ fn gather_ingredients() -> impl Action {
.debug(|| "gather ingredients") .debug(|| "gather ingredients")
} }
fn hunt_animals() -> impl Action {
just(|ctx| ctx.controller.do_hunt_animals()).debug(|| "hunt_animals")
}
fn find_forest(ctx: &NpcCtx) -> Option<Vec2<f32>> {
let chunk_pos = ctx.npc.wpos.xy().as_() / TerrainChunkSize::RECT_SIZE.as_();
Spiral2d::new()
.skip(thread_rng().gen_range(1..=8))
.take(49)
.map(|rpos| chunk_pos + rpos)
.find(|cpos| {
ctx.world
.sim()
.get(*cpos)
.map_or(false, |c| c.tree_density > 0.75 && c.surface_veg > 0.5)
})
.map(|chunk| TerrainChunkSize::center_wpos(chunk).as_())
}
fn villager(visiting_site: SiteId) -> impl Action { fn villager(visiting_site: SiteId) -> impl Action {
choose(move |ctx| { choose(move |ctx| {
/* /*
@ -586,20 +605,9 @@ fn villager(visiting_site: SiteId) -> impl Action {
); );
// Villagers with roles should perform those roles // Villagers with roles should perform those roles
} else if matches!(ctx.npc.profession, Some(Profession::Herbalist)) { } else if matches!(ctx.npc.profession, Some(Profession::Herbalist)) {
let chunk_pos = ctx.npc.wpos.xy().as_() / TerrainChunkSize::RECT_SIZE.as_(); if let Some(forest_wpos) = find_forest(ctx) {
if let Some(tree_chunk) = Spiral2d::new()
.skip(thread_rng().gen_range(1..=8))
.take(49)
.map(|rpos| chunk_pos + rpos)
.find(|cpos| {
ctx.world
.sim()
.get(*cpos)
.map_or(false, |c| c.tree_density > 0.75)
})
{
return important( return important(
travel_to_point(TerrainChunkSize::center_wpos(tree_chunk).as_()) travel_to_point(forest_wpos)
.debug(|| "walk to forest") .debug(|| "walk to forest")
.then({ .then({
let wait_time = thread_rng().gen_range(10.0..30.0); let wait_time = thread_rng().gen_range(10.0..30.0);
@ -608,6 +616,18 @@ fn villager(visiting_site: SiteId) -> impl Action {
.map(|_| ()), .map(|_| ()),
); );
} }
} else if matches!(ctx.npc.profession, Some(Profession::Hunter)) {
if let Some(forest_wpos) = find_forest(ctx) {
return important(
travel_to_point(forest_wpos)
.debug(|| "walk to forest")
.then({
let wait_time = thread_rng().gen_range(30.0..60.0);
hunt_animals().repeat().stop_if(timeout(wait_time))
})
.map(|_| ()),
);
}
} }
// If nothing else needs doing, walk between plazas and socialize // If nothing else needs doing, walk between plazas and socialize

View File

@ -246,7 +246,11 @@ impl Rule for SimulateNpcs {
} }
}, },
// When riding, other actions are disabled // When riding, other actions are disabled
Some(NpcActivity::Goto(_, _) | NpcActivity::Gather(_)) => {}, Some(
NpcActivity::Goto(_, _)
| NpcActivity::Gather(_)
| NpcActivity::HuntAnimals,
) => {},
None => {}, None => {},
} }
npc.wpos = vehicle.wpos; npc.wpos = vehicle.wpos;
@ -272,7 +276,7 @@ impl Rule for SimulateNpcs {
.with_z(0.0); .with_z(0.0);
} }
}, },
Some(NpcActivity::Gather(_)) => { Some(NpcActivity::Gather(_) | NpcActivity::HuntAnimals) => {
// TODO: Maybe they should walk around randomly // TODO: Maybe they should walk around randomly
// when gathering resources? // when gathering resources?
}, },

View File

@ -161,6 +161,7 @@ impl<'a> AgentData<'a> {
agent: &mut Agent, agent: &mut Agent,
controller: &mut Controller, controller: &mut Controller,
read_data: &ReadData, read_data: &ReadData,
event_emitter: &mut Emitter<ServerEvent>,
rng: &mut impl Rng, rng: &mut impl Rng,
) { ) {
enum ActionTimers { enum ActionTimers {
@ -213,6 +214,7 @@ impl<'a> AgentData<'a> {
} }
agent.action_state.timers[ActionTimers::TimerIdle as usize] = 0.0; agent.action_state.timers[ActionTimers::TimerIdle as usize] = 0.0;
'activity: {
match agent.rtsim_controller.activity { match agent.rtsim_controller.activity {
Some(NpcActivity::Goto(travel_to, speed_factor)) => { Some(NpcActivity::Goto(travel_to, speed_factor)) => {
// If it has an rtsim destination and can fly, then it should. // If it has an rtsim destination and can fly, then it should.
@ -279,7 +281,9 @@ impl<'a> AgentData<'a> {
- read_data - read_data
.world .world
.sim() .sim()
.get_alt_approx(self.pos.0.xy().map(|x: f32| x as i32)) .get_alt_approx(
self.pos.0.xy().map(|x: f32| x as i32),
)
.unwrap_or(0.0); .unwrap_or(0.0);
#[cfg(not(feature = "worldgen"))] #[cfg(not(feature = "worldgen"))]
let height_approx = self.pos.0.z; let height_approx = self.pos.0.z;
@ -293,7 +297,8 @@ impl<'a> AgentData<'a> {
// NOTE: costs 15-20 us (imbris) // NOTE: costs 15-20 us (imbris)
for i in 0..=NUM_RAYS { for i in 0..=NUM_RAYS {
let magnitude = self.body.map_or(20.0, |b| b.flying_height()); let magnitude = self.body.map_or(20.0, |b| b.flying_height());
// Lerp between a line straight ahead and straight down to detect a // Lerp between a line straight ahead and straight down to
// detect a
// wedge of obstacles we might fly into (inclusive so that both // wedge of obstacles we might fly into (inclusive so that both
// vectors are sampled) // vectors are sampled)
if let Some(dir) = Lerp::lerp( if let Some(dir) = Lerp::lerp(
@ -337,12 +342,27 @@ impl<'a> AgentData<'a> {
controller.push_action(ControlAction::Unwield); controller.push_action(ControlAction::Unwield);
} }
} }
break 'activity; // Don't fall through to idle wandering
}, },
Some(NpcActivity::Gather(resources)) => { Some(NpcActivity::Gather(resources)) => {
// TODO: Implement // TODO: Implement
controller.push_action(ControlAction::Dance); controller.push_action(ControlAction::Dance);
break 'activity; // Don't fall through to idle wandering
}, },
None => { Some(NpcActivity::HuntAnimals) => {
if rng.gen::<f32>() < 0.1 {
self.choose_target(
agent,
controller,
read_data,
event_emitter,
AgentData::is_hunting_animal,
);
}
},
None => {},
}
// Bats should fly // Bats should fly
// Use a proportional controller as the bouncing effect mimics bat flight // Use a proportional controller as the bouncing effect mimics bat flight
if self.traversal_config.can_fly if self.traversal_config.can_fly
@ -455,7 +475,6 @@ impl<'a> AgentData<'a> {
if rng.gen::<f32>() < 0.0035 { if rng.gen::<f32>() < 0.0035 {
controller.push_action(ControlAction::Sit); controller.push_action(ControlAction::Sit);
} }
},
} }
} }
@ -660,6 +679,7 @@ impl<'a> AgentData<'a> {
controller: &mut Controller, controller: &mut Controller,
read_data: &ReadData, read_data: &ReadData,
event_emitter: &mut Emitter<ServerEvent>, event_emitter: &mut Emitter<ServerEvent>,
is_enemy: fn(&Self, EcsEntity, &ReadData) -> bool,
) { ) {
enum ActionStateTimers { enum ActionStateTimers {
TimerChooseTarget = 0, TimerChooseTarget = 0,
@ -700,7 +720,7 @@ impl<'a> AgentData<'a> {
let get_pos = |entity| read_data.positions.get(entity); let get_pos = |entity| read_data.positions.get(entity);
let get_enemy = |(entity, attack_target): (EcsEntity, bool)| { let get_enemy = |(entity, attack_target): (EcsEntity, bool)| {
if attack_target { if attack_target {
if self.is_enemy(entity, read_data) { if is_enemy(self, entity, read_data) {
Some((entity, true)) Some((entity, true))
} else if can_ambush(entity, read_data) { } else if can_ambush(entity, read_data) {
controller.clone().push_utterance(UtteranceKind::Ambush); controller.clone().push_utterance(UtteranceKind::Ambush);
@ -1385,12 +1405,13 @@ impl<'a> AgentData<'a> {
agent: &mut Agent, agent: &mut Agent,
controller: &mut Controller, controller: &mut Controller,
read_data: &ReadData, read_data: &ReadData,
event_emitter: &mut Emitter<ServerEvent>,
rng: &mut impl Rng, rng: &mut impl Rng,
) { ) {
agent.forget_old_sounds(read_data.time.0); agent.forget_old_sounds(read_data.time.0);
if is_invulnerable(*self.entity, read_data) { if is_invulnerable(*self.entity, read_data) {
self.idle(agent, controller, read_data, rng); self.idle(agent, controller, read_data, event_emitter, rng);
return; return;
} }
@ -1423,13 +1444,13 @@ impl<'a> AgentData<'a> {
} else if self.below_flee_health(agent) || !follows_threatening_sounds { } else if self.below_flee_health(agent) || !follows_threatening_sounds {
self.flee(agent, controller, &sound_pos, &read_data.terrain); self.flee(agent, controller, &sound_pos, &read_data.terrain);
} else { } else {
self.idle(agent, controller, read_data, rng); self.idle(agent, controller, read_data, event_emitter, rng);
} }
} else { } else {
self.idle(agent, controller, read_data, rng); self.idle(agent, controller, read_data, event_emitter, rng);
} }
} else { } else {
self.idle(agent, controller, read_data, rng); self.idle(agent, controller, read_data, event_emitter, rng);
} }
} }
@ -1438,6 +1459,7 @@ impl<'a> AgentData<'a> {
agent: &mut Agent, agent: &mut Agent,
read_data: &ReadData, read_data: &ReadData,
controller: &mut Controller, controller: &mut Controller,
event_emitter: &mut Emitter<ServerEvent>,
rng: &mut impl Rng, rng: &mut impl Rng,
) { ) {
if let Some(Target { target, .. }) = agent.target { if let Some(Target { target, .. }) = agent.target {
@ -1467,7 +1489,7 @@ impl<'a> AgentData<'a> {
Some(tgt_pos.0), Some(tgt_pos.0),
)); ));
self.idle(agent, controller, read_data, rng); self.idle(agent, controller, read_data, event_emitter, rng);
} else { } else {
let target_data = TargetData::new(tgt_pos, target, read_data); let target_data = TargetData::new(tgt_pos, target, read_data);
// TODO: Reimplement this in rtsim // TODO: Reimplement this in rtsim
@ -1595,7 +1617,7 @@ impl<'a> AgentData<'a> {
}) })
} }
fn is_enemy(&self, entity: EcsEntity, read_data: &ReadData) -> bool { pub fn is_enemy(&self, entity: EcsEntity, read_data: &ReadData) -> bool {
let other_alignment = read_data.alignments.get(entity); let other_alignment = read_data.alignments.get(entity);
(entity != *self.entity) (entity != *self.entity)
@ -1604,6 +1626,11 @@ impl<'a> AgentData<'a> {
|| (is_villager(self.alignment) && is_dressed_as_cultist(entity, read_data))) || (is_villager(self.alignment) && is_dressed_as_cultist(entity, read_data)))
} }
pub fn is_hunting_animal(&self, entity: EcsEntity, read_data: &ReadData) -> bool {
(entity != *self.entity)
&& matches!(read_data.bodies.get(entity), Some(Body::QuadrupedSmall(_)))
}
fn should_defend(&self, entity: EcsEntity, read_data: &ReadData) -> bool { fn should_defend(&self, entity: EcsEntity, read_data: &ReadData) -> bool {
let entity_alignment = read_data.alignments.get(entity); let entity_alignment = read_data.alignments.get(entity);

View File

@ -437,6 +437,7 @@ fn attack_if_owner_hurt(bdata: &mut BehaviorData) -> bool {
bdata.agent, bdata.agent,
bdata.read_data, bdata.read_data,
bdata.controller, bdata.controller,
bdata.event_emitter,
bdata.rng, bdata.rng,
); );
return true; return true;
@ -543,12 +544,14 @@ fn handle_timed_events(bdata: &mut BehaviorData) -> bool {
bdata.controller, bdata.controller,
bdata.read_data, bdata.read_data,
bdata.event_emitter, bdata.event_emitter,
AgentData::is_enemy,
); );
} else { } else {
bdata.agent_data.handle_sounds_heard( bdata.agent_data.handle_sounds_heard(
bdata.agent, bdata.agent,
bdata.controller, bdata.controller,
bdata.read_data, bdata.read_data,
bdata.event_emitter,
bdata.rng, bdata.rng,
); );
} }
@ -763,12 +766,12 @@ fn do_combat(bdata: &mut BehaviorData) -> bool {
[ActionStateBehaviorTreeTimers::TimerBehaviorTree as usize] = 0.0; [ActionStateBehaviorTreeTimers::TimerBehaviorTree as usize] = 0.0;
agent.target = None; agent.target = None;
agent.flee_from_pos = None; agent.flee_from_pos = None;
agent_data.idle(agent, controller, read_data, rng); agent_data.idle(agent, controller, read_data, event_emitter, rng);
} }
} else if is_dead(target, read_data) { } else if is_dead(target, read_data) {
agent_data.exclaim_relief_about_enemy_dead(agent, event_emitter); agent_data.exclaim_relief_about_enemy_dead(agent, event_emitter);
agent.target = None; agent.target = None;
agent_data.idle(agent, controller, read_data, rng); agent_data.idle(agent, controller, read_data, event_emitter, rng);
} else if is_invulnerable(target, read_data) } else if is_invulnerable(target, read_data)
|| stop_pursuing( || stop_pursuing(
dist_sqrd, dist_sqrd,
@ -780,13 +783,19 @@ fn do_combat(bdata: &mut BehaviorData) -> bool {
) )
{ {
agent.target = None; agent.target = None;
agent_data.idle(agent, controller, read_data, rng); agent_data.idle(agent, controller, read_data, event_emitter, rng);
} else { } else {
let is_time_to_retarget = let is_time_to_retarget =
read_data.time.0 - selected_at > RETARGETING_THRESHOLD_SECONDS; read_data.time.0 - selected_at > RETARGETING_THRESHOLD_SECONDS;
if !in_aggro_range && is_time_to_retarget { if !in_aggro_range && is_time_to_retarget {
agent_data.choose_target(agent, controller, read_data, event_emitter); agent_data.choose_target(
agent,
controller,
read_data,
event_emitter,
AgentData::is_enemy,
);
} }
if aggro_on { if aggro_on {