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)
Goto(Vec3<f32>, f32),
Gather(&'static [ChunkResource]),
// TODO: Generalise to other entities? What kinds of animals?
HuntAnimals,
}
#[derive(Clone, Copy, Debug)]

View File

@ -62,6 +62,8 @@ impl Controller {
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)); }
}

View File

@ -528,6 +528,25 @@ fn gather_ingredients() -> impl Action {
.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 {
choose(move |ctx| {
/*
@ -586,20 +605,9 @@ fn villager(visiting_site: SiteId) -> impl Action {
);
// Villagers with roles should perform those roles
} else if matches!(ctx.npc.profession, Some(Profession::Herbalist)) {
let chunk_pos = ctx.npc.wpos.xy().as_() / TerrainChunkSize::RECT_SIZE.as_();
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)
})
{
if let Some(forest_wpos) = find_forest(ctx) {
return important(
travel_to_point(TerrainChunkSize::center_wpos(tree_chunk).as_())
travel_to_point(forest_wpos)
.debug(|| "walk to forest")
.then({
let wait_time = thread_rng().gen_range(10.0..30.0);
@ -608,6 +616,18 @@ fn villager(visiting_site: SiteId) -> impl Action {
.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

View File

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

View File

@ -161,6 +161,7 @@ impl<'a> AgentData<'a> {
agent: &mut Agent,
controller: &mut Controller,
read_data: &ReadData,
event_emitter: &mut Emitter<ServerEvent>,
rng: &mut impl Rng,
) {
enum ActionTimers {
@ -213,249 +214,267 @@ impl<'a> AgentData<'a> {
}
agent.action_state.timers[ActionTimers::TimerIdle as usize] = 0.0;
match agent.rtsim_controller.activity {
Some(NpcActivity::Goto(travel_to, speed_factor)) => {
// If it has an rtsim destination and can fly, then it should.
// If it is flying and bumps something above it, then it should move down.
if self.traversal_config.can_fly
&& !read_data
.terrain
.ray(self.pos.0, self.pos.0 + (Vec3::unit_z() * 3.0))
.until(Block::is_solid)
.cast()
.1
.map_or(true, |b| b.is_some())
{
controller.push_basic_input(InputKind::Fly);
} else {
controller.push_cancel_input(InputKind::Fly)
}
let chase_tgt = read_data
.terrain
.try_find_space(travel_to.as_())
.map(|pos| pos.as_())
.unwrap_or(travel_to);
if let Some((bearing, speed)) = agent.chaser.chase(
&*read_data.terrain,
self.pos.0,
self.vel.0,
chase_tgt,
TraversalConfig {
min_tgt_dist: 1.25,
..self.traversal_config
},
) {
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero)
* speed.min(speed_factor);
self.jump_if(bearing.z > 1.5 || self.traversal_config.can_fly, controller);
controller.inputs.climb = Some(comp::Climb::Up);
//.filter(|_| bearing.z > 0.1 || self.physics_state.in_liquid().is_some());
let height_offset = bearing.z
+ if self.traversal_config.can_fly {
// NOTE: costs 4 us (imbris)
let obstacle_ahead = read_data
.terrain
.ray(
self.pos.0 + Vec3::unit_z(),
self.pos.0
+ bearing.try_normalized().unwrap_or_else(Vec3::unit_y)
* 80.0
+ Vec3::unit_z(),
)
.until(Block::is_solid)
.cast()
.1
.map_or(true, |b| b.is_some());
let mut ground_too_close = self
.body
.map(|body| {
#[cfg(feature = "worldgen")]
let height_approx = self.pos.0.z
- read_data
.world
.sim()
.get_alt_approx(self.pos.0.xy().map(|x: f32| x as i32))
.unwrap_or(0.0);
#[cfg(not(feature = "worldgen"))]
let height_approx = self.pos.0.z;
height_approx < body.flying_height()
})
.unwrap_or(false);
const NUM_RAYS: usize = 5;
// NOTE: costs 15-20 us (imbris)
for i in 0..=NUM_RAYS {
let magnitude = self.body.map_or(20.0, |b| b.flying_height());
// Lerp between a line straight ahead and straight down to detect a
// wedge of obstacles we might fly into (inclusive so that both
// vectors are sampled)
if let Some(dir) = Lerp::lerp(
-Vec3::unit_z(),
Vec3::new(bearing.x, bearing.y, 0.0),
i as f32 / NUM_RAYS as f32,
)
.try_normalized()
{
ground_too_close |= read_data
.terrain
.ray(self.pos.0, self.pos.0 + magnitude * dir)
.until(|b: &Block| b.is_solid() || b.is_liquid())
.cast()
.1
.map_or(false, |b| b.is_some())
}
}
if obstacle_ahead || ground_too_close {
5.0 //fly up when approaching obstacles
} else {
-2.0
} //flying things should slowly come down from the stratosphere
} else {
0.05 //normal land traveller offset
};
if let Some(pid) = agent.position_pid_controller.as_mut() {
pid.sp = self.pos.0.z + height_offset * Vec3::unit_z();
controller.inputs.move_z = pid.calc_err();
} else {
controller.inputs.move_z = height_offset;
}
// Put away weapon
if rng.gen_bool(0.1)
&& matches!(
read_data.char_states.get(*self.entity),
Some(CharacterState::Wielding(_))
)
'activity: {
match agent.rtsim_controller.activity {
Some(NpcActivity::Goto(travel_to, speed_factor)) => {
// If it has an rtsim destination and can fly, then it should.
// If it is flying and bumps something above it, then it should move down.
if self.traversal_config.can_fly
&& !read_data
.terrain
.ray(self.pos.0, self.pos.0 + (Vec3::unit_z() * 3.0))
.until(Block::is_solid)
.cast()
.1
.map_or(true, |b| b.is_some())
{
controller.push_action(ControlAction::Unwield);
controller.push_basic_input(InputKind::Fly);
} else {
controller.push_cancel_input(InputKind::Fly)
}
}
},
Some(NpcActivity::Gather(resources)) => {
// TODO: Implement
controller.push_action(ControlAction::Dance);
},
None => {
// Bats should fly
// Use a proportional controller as the bouncing effect mimics bat flight
if self.traversal_config.can_fly
&& self
.inventory
.equipped(EquipSlot::ActiveMainhand)
.as_ref()
.map_or(false, |item| {
item.ability_spec().map_or(false, |a_s| match &*a_s {
AbilitySpec::Custom(spec) => {
matches!(
spec.as_str(),
"Simple Flying Melee"
| "Flame Wyvern"
| "Frost Wyvern"
| "Cloud Wyvern"
| "Sea Wyvern"
| "Weald Wyvern"
)
},
_ => false,
})
})
{
// Bats don't like the ground, so make sure they are always flying
controller.push_basic_input(InputKind::Fly);
// Use a proportional controller with a coefficient of 1.0 to
// maintain altitude
let alt = read_data
.terrain
.ray(self.pos.0, self.pos.0 - (Vec3::unit_z() * 7.0))
.until(Block::is_solid)
.cast()
.0;
let set_point = 5.0;
let error = set_point - alt;
controller.inputs.move_z = error;
// If on the ground, jump
if self.physics_state.on_ground.is_some() {
controller.push_basic_input(InputKind::Jump);
}
}
agent.bearing += Vec2::new(rng.gen::<f32>() - 0.5, rng.gen::<f32>() - 0.5) * 0.1
- agent.bearing * 0.003
- agent.patrol_origin.map_or(Vec2::zero(), |patrol_origin| {
(self.pos.0 - patrol_origin).xy() * 0.0002
});
// Stop if we're too close to a wall
// or about to walk off a cliff
// NOTE: costs 1 us (imbris) <- before cliff raycast added
agent.bearing *= 0.1
+ if read_data
let chase_tgt = read_data
.terrain
.try_find_space(travel_to.as_())
.map(|pos| pos.as_())
.unwrap_or(travel_to);
if let Some((bearing, speed)) = agent.chaser.chase(
&*read_data.terrain,
self.pos.0,
self.vel.0,
chase_tgt,
TraversalConfig {
min_tgt_dist: 1.25,
..self.traversal_config
},
) {
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero)
* speed.min(speed_factor);
self.jump_if(bearing.z > 1.5 || self.traversal_config.can_fly, controller);
controller.inputs.climb = Some(comp::Climb::Up);
//.filter(|_| bearing.z > 0.1 || self.physics_state.in_liquid().is_some());
let height_offset = bearing.z
+ if self.traversal_config.can_fly {
// NOTE: costs 4 us (imbris)
let obstacle_ahead = read_data
.terrain
.ray(
self.pos.0 + Vec3::unit_z(),
self.pos.0
+ bearing.try_normalized().unwrap_or_else(Vec3::unit_y)
* 80.0
+ Vec3::unit_z(),
)
.until(Block::is_solid)
.cast()
.1
.map_or(true, |b| b.is_some());
let mut ground_too_close = self
.body
.map(|body| {
#[cfg(feature = "worldgen")]
let height_approx = self.pos.0.z
- read_data
.world
.sim()
.get_alt_approx(
self.pos.0.xy().map(|x: f32| x as i32),
)
.unwrap_or(0.0);
#[cfg(not(feature = "worldgen"))]
let height_approx = self.pos.0.z;
height_approx < body.flying_height()
})
.unwrap_or(false);
const NUM_RAYS: usize = 5;
// NOTE: costs 15-20 us (imbris)
for i in 0..=NUM_RAYS {
let magnitude = self.body.map_or(20.0, |b| b.flying_height());
// Lerp between a line straight ahead and straight down to
// detect a
// wedge of obstacles we might fly into (inclusive so that both
// vectors are sampled)
if let Some(dir) = Lerp::lerp(
-Vec3::unit_z(),
Vec3::new(bearing.x, bearing.y, 0.0),
i as f32 / NUM_RAYS as f32,
)
.try_normalized()
{
ground_too_close |= read_data
.terrain
.ray(self.pos.0, self.pos.0 + magnitude * dir)
.until(|b: &Block| b.is_solid() || b.is_liquid())
.cast()
.1
.map_or(false, |b| b.is_some())
}
}
if obstacle_ahead || ground_too_close {
5.0 //fly up when approaching obstacles
} else {
-2.0
} //flying things should slowly come down from the stratosphere
} else {
0.05 //normal land traveller offset
};
if let Some(pid) = agent.position_pid_controller.as_mut() {
pid.sp = self.pos.0.z + height_offset * Vec3::unit_z();
controller.inputs.move_z = pid.calc_err();
} else {
controller.inputs.move_z = height_offset;
}
// Put away weapon
if rng.gen_bool(0.1)
&& matches!(
read_data.char_states.get(*self.entity),
Some(CharacterState::Wielding(_))
)
{
controller.push_action(ControlAction::Unwield);
}
}
break 'activity; // Don't fall through to idle wandering
},
Some(NpcActivity::Gather(resources)) => {
// TODO: Implement
controller.push_action(ControlAction::Dance);
break 'activity; // Don't fall through to idle wandering
},
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
// Use a proportional controller as the bouncing effect mimics bat flight
if self.traversal_config.can_fly
&& self
.inventory
.equipped(EquipSlot::ActiveMainhand)
.as_ref()
.map_or(false, |item| {
item.ability_spec().map_or(false, |a_s| match &*a_s {
AbilitySpec::Custom(spec) => {
matches!(
spec.as_str(),
"Simple Flying Melee"
| "Flame Wyvern"
| "Frost Wyvern"
| "Cloud Wyvern"
| "Sea Wyvern"
| "Weald Wyvern"
)
},
_ => false,
})
})
{
// Bats don't like the ground, so make sure they are always flying
controller.push_basic_input(InputKind::Fly);
// Use a proportional controller with a coefficient of 1.0 to
// maintain altitude
let alt = read_data
.terrain
.ray(self.pos.0, self.pos.0 - (Vec3::unit_z() * 7.0))
.until(Block::is_solid)
.cast()
.0;
let set_point = 5.0;
let error = set_point - alt;
controller.inputs.move_z = error;
// If on the ground, jump
if self.physics_state.on_ground.is_some() {
controller.push_basic_input(InputKind::Jump);
}
}
agent.bearing += Vec2::new(rng.gen::<f32>() - 0.5, rng.gen::<f32>() - 0.5) * 0.1
- agent.bearing * 0.003
- agent.patrol_origin.map_or(Vec2::zero(), |patrol_origin| {
(self.pos.0 - patrol_origin).xy() * 0.0002
});
// Stop if we're too close to a wall
// or about to walk off a cliff
// NOTE: costs 1 us (imbris) <- before cliff raycast added
agent.bearing *= 0.1
+ if read_data
.terrain
.ray(
self.pos.0 + Vec3::unit_z(),
self.pos.0
+ Vec3::from(agent.bearing)
.try_normalized()
.unwrap_or_else(Vec3::unit_y)
* 5.0
+ Vec3::unit_z(),
)
.until(Block::is_solid)
.cast()
.1
.map_or(true, |b| b.is_none())
&& read_data
.terrain
.ray(
self.pos.0 + Vec3::unit_z(),
self.pos.0
+ Vec3::from(agent.bearing)
.try_normalized()
.unwrap_or_else(Vec3::unit_y),
self.pos.0
+ Vec3::from(agent.bearing)
.try_normalized()
.unwrap_or_else(Vec3::unit_y)
* 5.0
+ Vec3::unit_z(),
- Vec3::unit_z() * 4.0,
)
.until(Block::is_solid)
.cast()
.1
.map_or(true, |b| b.is_none())
&& read_data
.terrain
.ray(
self.pos.0
+ Vec3::from(agent.bearing)
.try_normalized()
.unwrap_or_else(Vec3::unit_y),
self.pos.0
+ Vec3::from(agent.bearing)
.try_normalized()
.unwrap_or_else(Vec3::unit_y)
- Vec3::unit_z() * 4.0,
)
.until(Block::is_solid)
.cast()
.0
< 3.0
{
0.9
} else {
0.0
};
if agent.bearing.magnitude_squared() > 0.5f32.powi(2) {
controller.inputs.move_dir = agent.bearing * 0.65;
}
// Put away weapon
if rng.gen_bool(0.1)
&& matches!(
read_data.char_states.get(*self.entity),
Some(CharacterState::Wielding(_))
)
.0
< 3.0
{
controller.push_action(ControlAction::Unwield);
}
0.9
} else {
0.0
};
if rng.gen::<f32>() < 0.0015 {
controller.push_utterance(UtteranceKind::Calm);
}
if agent.bearing.magnitude_squared() > 0.5f32.powi(2) {
controller.inputs.move_dir = agent.bearing * 0.65;
}
// Sit
if rng.gen::<f32>() < 0.0035 {
controller.push_action(ControlAction::Sit);
}
},
// Put away weapon
if rng.gen_bool(0.1)
&& matches!(
read_data.char_states.get(*self.entity),
Some(CharacterState::Wielding(_))
)
{
controller.push_action(ControlAction::Unwield);
}
if rng.gen::<f32>() < 0.0015 {
controller.push_utterance(UtteranceKind::Calm);
}
// Sit
if rng.gen::<f32>() < 0.0035 {
controller.push_action(ControlAction::Sit);
}
}
}
@ -660,6 +679,7 @@ impl<'a> AgentData<'a> {
controller: &mut Controller,
read_data: &ReadData,
event_emitter: &mut Emitter<ServerEvent>,
is_enemy: fn(&Self, EcsEntity, &ReadData) -> bool,
) {
enum ActionStateTimers {
TimerChooseTarget = 0,
@ -700,7 +720,7 @@ impl<'a> AgentData<'a> {
let get_pos = |entity| read_data.positions.get(entity);
let get_enemy = |(entity, attack_target): (EcsEntity, bool)| {
if attack_target {
if self.is_enemy(entity, read_data) {
if is_enemy(self, entity, read_data) {
Some((entity, true))
} else if can_ambush(entity, read_data) {
controller.clone().push_utterance(UtteranceKind::Ambush);
@ -1385,12 +1405,13 @@ impl<'a> AgentData<'a> {
agent: &mut Agent,
controller: &mut Controller,
read_data: &ReadData,
event_emitter: &mut Emitter<ServerEvent>,
rng: &mut impl Rng,
) {
agent.forget_old_sounds(read_data.time.0);
if is_invulnerable(*self.entity, read_data) {
self.idle(agent, controller, read_data, rng);
self.idle(agent, controller, read_data, event_emitter, rng);
return;
}
@ -1423,13 +1444,13 @@ impl<'a> AgentData<'a> {
} else if self.below_flee_health(agent) || !follows_threatening_sounds {
self.flee(agent, controller, &sound_pos, &read_data.terrain);
} else {
self.idle(agent, controller, read_data, rng);
self.idle(agent, controller, read_data, event_emitter, rng);
}
} else {
self.idle(agent, controller, read_data, rng);
self.idle(agent, controller, read_data, event_emitter, rng);
}
} 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,
read_data: &ReadData,
controller: &mut Controller,
event_emitter: &mut Emitter<ServerEvent>,
rng: &mut impl Rng,
) {
if let Some(Target { target, .. }) = agent.target {
@ -1467,7 +1489,7 @@ impl<'a> AgentData<'a> {
Some(tgt_pos.0),
));
self.idle(agent, controller, read_data, rng);
self.idle(agent, controller, read_data, event_emitter, rng);
} else {
let target_data = TargetData::new(tgt_pos, target, read_data);
// 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);
(entity != *self.entity)
@ -1604,6 +1626,11 @@ impl<'a> AgentData<'a> {
|| (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 {
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.read_data,
bdata.controller,
bdata.event_emitter,
bdata.rng,
);
return true;
@ -543,12 +544,14 @@ fn handle_timed_events(bdata: &mut BehaviorData) -> bool {
bdata.controller,
bdata.read_data,
bdata.event_emitter,
AgentData::is_enemy,
);
} else {
bdata.agent_data.handle_sounds_heard(
bdata.agent,
bdata.controller,
bdata.read_data,
bdata.event_emitter,
bdata.rng,
);
}
@ -763,12 +766,12 @@ fn do_combat(bdata: &mut BehaviorData) -> bool {
[ActionStateBehaviorTreeTimers::TimerBehaviorTree as usize] = 0.0;
agent.target = 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) {
agent_data.exclaim_relief_about_enemy_dead(agent, event_emitter);
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)
|| stop_pursuing(
dist_sqrd,
@ -780,13 +783,19 @@ fn do_combat(bdata: &mut BehaviorData) -> bool {
)
{
agent.target = None;
agent_data.idle(agent, controller, read_data, rng);
agent_data.idle(agent, controller, read_data, event_emitter, rng);
} else {
let is_time_to_retarget =
read_data.time.0 - selected_at > RETARGETING_THRESHOLD_SECONDS;
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 {