Added CharacterActivity, made NPCs look at the player when speaking to them

This commit is contained in:
Joshua Barretto 2023-04-04 15:55:57 +01:00
parent 85c572f6e2
commit 3e0f5295c0
27 changed files with 491 additions and 257 deletions

View File

@ -40,6 +40,7 @@ macro_rules! synced_components {
sticky: Sticky,
immovable: Immovable,
character_state: CharacterState,
character_activity: CharacterActivity,
shockwave: Shockwave,
beam_segment: BeamSegment,
alignment: Alignment,
@ -201,6 +202,10 @@ impl NetSync for CharacterState {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}
impl NetSync for CharacterActivity {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}
impl NetSync for Shockwave {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}

View File

@ -625,6 +625,11 @@ impl Awareness {
self.reached = false;
}
}
pub fn set_maximally_aware(&mut self) {
self.reached = true;
self.level = Self::ALERT;
}
}
#[derive(Clone, Debug, PartialOrd, PartialEq, Eq)]

View File

@ -983,7 +983,7 @@ impl Body {
}
/// Returns the eye height for this creature.
pub fn eye_height(&self) -> f32 { self.height() * 0.9 }
pub fn eye_height(&self, scale: f32) -> f32 { self.height() * 0.9 * scale }
pub fn default_light_offset(&self) -> Vec3<f32> {
// TODO: Make this a manifest

View File

@ -12,6 +12,7 @@ use crate::{
utils::{AbilityInfo, StageSection},
*,
},
util::Dir,
};
use serde::{Deserialize, Serialize};
use specs::{Component, DerefFlaggedStorage};
@ -30,6 +31,7 @@ pub struct StateUpdate {
pub should_strafe: bool,
pub queued_inputs: BTreeMap<InputKind, InputAttr>,
pub removed_inputs: Vec<InputKind>,
pub character_activity: CharacterActivity,
}
pub struct OutputEvents<'a> {
@ -60,6 +62,7 @@ impl From<&JoinData<'_>> for StateUpdate {
character: data.character.clone(),
queued_inputs: BTreeMap::new(),
removed_inputs: Vec::new(),
character_activity: *data.character_activity,
}
}
}
@ -979,3 +982,20 @@ impl Default for CharacterState {
impl Component for CharacterState {
type Storage = DerefFlaggedStorage<Self, specs::VecStorage<Self>>;
}
/// Contains information about the visual activity of a character.
///
/// For now this only includes the direction they're looking in, but later it
/// might include markers indicating that they're available for
/// trade/interaction, more details about their stance or appearance, facial
/// expression, etc.
#[derive(Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct CharacterActivity {
/// `None` means that the look direction should be derived from the
/// orientation
pub look_dir: Option<Dir>,
}
impl Component for CharacterActivity {
type Storage = DerefFlaggedStorage<Self, specs::VecStorage<Self>>;
}

View File

@ -1,4 +1,5 @@
use vek::Vec2;
// TODO: Move this to common/src/, it's not a component
/// Cardinal directions
pub enum Direction {

View File

@ -73,7 +73,7 @@ pub use self::{
Buff, BuffCategory, BuffChange, BuffData, BuffEffect, BuffId, BuffKind, BuffSource, Buffs,
ModifierKind,
},
character_state::{CharacterState, StateUpdate},
character_state::{CharacterActivity, CharacterState, StateUpdate},
chat::{
ChatMode, ChatMsg, ChatType, Faction, SpeechBubble, SpeechBubbleType, UnresolvedChatMsg,
},

View File

@ -91,7 +91,10 @@ impl CharacterBehavior for Data {
// Shoots all projectiles simultaneously
for i in 0..self.static_data.num_projectiles {
// Gets offsets
let body_offsets = data.body.projectile_offsets(update.ori.look_vec());
let body_offsets = data.body.projectile_offsets(
update.ori.look_vec(),
data.scale.map_or(1.0, |s| s.0),
);
let pos = Pos(data.pos.0 + body_offsets);
// Adds a slight spread to the projectiles. First projectile has no spread,
// and spread increases linearly with number of projectiles created.

View File

@ -149,7 +149,7 @@ impl CharacterBehavior for Data {
let collision_vector = Vec3::new(
data.pos.0.x + (summon_frac * 2.0 * PI).sin() * obstacle_xy,
data.pos.0.y + (summon_frac * 2.0 * PI).cos() * obstacle_xy,
data.pos.0.z + data.body.eye_height(),
data.pos.0.z + data.body.eye_height(data.scale.map_or(1.0, |s| s.0)),
);
// Check for collision in z up to 50 blocks

View File

@ -3,8 +3,8 @@ use crate::{
self,
character_state::OutputEvents,
item::{tool::AbilityMap, MaterialStatManifest},
ActiveAbilities, Beam, Body, CharacterState, Combo, ControlAction, Controller,
ControllerInputs, Density, Energy, Health, InputAttr, InputKind, Inventory,
ActiveAbilities, Beam, Body, CharacterActivity, CharacterState, Combo, ControlAction,
Controller, ControllerInputs, Density, Energy, Health, InputAttr, InputKind, Inventory,
InventoryAction, Mass, Melee, Ori, PhysicsState, Pos, Scale, SkillSet, Stance, StateUpdate,
Stats, Vel,
},
@ -120,6 +120,7 @@ pub struct JoinData<'a> {
pub entity: Entity,
pub uid: &'a Uid,
pub character: &'a CharacterState,
pub character_activity: &'a CharacterActivity,
pub pos: &'a Pos,
pub vel: &'a Vel,
pub ori: &'a Ori,
@ -153,6 +154,7 @@ pub struct JoinStruct<'a> {
pub entity: Entity,
pub uid: &'a Uid,
pub char_state: FlaggedAccessMut<'a, &'a mut CharacterState, CharacterState>,
pub character_activity: FlaggedAccessMut<'a, &'a mut CharacterActivity, CharacterActivity>,
pub pos: &'a mut Pos,
pub vel: &'a mut Vel,
pub ori: &'a mut Ori,
@ -190,6 +192,7 @@ impl<'a> JoinData<'a> {
entity: j.entity,
uid: j.uid,
character: &j.char_state,
character_activity: &j.character_activity,
pos: j.pos,
vel: j.vel,
ori: j.ori,

View File

@ -114,7 +114,9 @@ impl CharacterBehavior for Data {
get_crit_data(data, self.static_data.ability_info);
let tool_stats = get_tool_stats(data, self.static_data.ability_info);
// Gets offsets
let body_offsets = data.body.projectile_offsets(update.ori.look_vec());
let body_offsets = data
.body
.projectile_offsets(update.ori.look_vec(), data.scale.map_or(1.0, |s| s.0));
let pos = Pos(data.pos.0 + body_offsets);
let projectile = arrow.create_projectile(
Some(*data.uid),

View File

@ -95,7 +95,9 @@ impl CharacterBehavior for Data {
get_crit_data(data, self.static_data.ability_info);
let tool_stats = get_tool_stats(data, self.static_data.ability_info);
// Gets offsets
let body_offsets = data.body.projectile_offsets(update.ori.look_vec());
let body_offsets = data
.body
.projectile_offsets(update.ori.look_vec(), data.scale.map_or(1.0, |s| s.0));
let pos = Pos(data.pos.0 + body_offsets);
let projectile = self.static_data.projectile.create_projectile(
Some(*data.uid),

View File

@ -298,10 +298,10 @@ impl Body {
/// Returns the position where a projectile should be fired relative to this
/// body
pub fn projectile_offsets(&self, ori: Vec3<f32>) -> Vec3<f32> {
pub fn projectile_offsets(&self, ori: Vec3<f32>, scale: f32) -> Vec3<f32> {
let body_offsets_z = match self {
Body::Golem(_) => self.height() * 0.4,
_ => self.eye_height(),
_ => self.eye_height(scale),
};
let dim = self.dimensions();
@ -611,6 +611,9 @@ pub fn handle_orientation(
.ori
.slerped_towards(target_ori, target_fraction.min(1.0))
};
// Look at things
update.character_activity.look_dir = Some(data.controller.inputs.look_dir);
}
/// Updates components to move player as if theyre swimming

View File

@ -199,6 +199,7 @@ impl State {
ecs.register::<comp::Sticky>();
ecs.register::<comp::Immovable>();
ecs.register::<comp::CharacterState>();
ecs.register::<comp::CharacterActivity>();
ecs.register::<comp::Object>();
ecs.register::<comp::Group>();
ecs.register::<comp::Shockwave>();

View File

@ -8,9 +8,9 @@ use common::{
self,
character_state::OutputEvents,
inventory::item::{tool::AbilityMap, MaterialStatManifest},
ActiveAbilities, Beam, Body, CharacterState, Combo, Controller, Density, Energy, Health,
Inventory, InventoryManip, Mass, Melee, Ori, PhysicsState, Poise, Pos, Scale, SkillSet,
Stance, StateUpdate, Stats, Vel,
ActiveAbilities, Beam, Body, CharacterActivity, CharacterState, Combo, Controller, Density,
Energy, Health, Inventory, InventoryManip, Mass, Melee, Ori, PhysicsState, Poise, Pos,
Scale, SkillSet, Stance, StateUpdate, Stats, Vel,
},
event::{EventBus, LocalEvent, ServerEvent},
link::Is,
@ -65,6 +65,7 @@ impl<'a> System<'a> for Sys {
type SystemData = (
ReadData<'a>,
WriteStorage<'a, CharacterState>,
WriteStorage<'a, CharacterActivity>,
WriteStorage<'a, Pos>,
WriteStorage<'a, Vel>,
WriteStorage<'a, Ori>,
@ -84,6 +85,7 @@ impl<'a> System<'a> for Sys {
(
read_data,
mut character_states,
mut character_activities,
mut positions,
mut velocities,
mut orientations,
@ -106,6 +108,7 @@ impl<'a> System<'a> for Sys {
entity,
uid,
mut char_state,
character_activity,
pos,
vel,
ori,
@ -116,13 +119,13 @@ impl<'a> System<'a> for Sys {
controller,
health,
body,
physics,
(scale, stat, skill_set, active_abilities, is_rider),
(physics, scale, stat, skill_set, active_abilities, is_rider),
combo,
) in (
&read_data.entities,
&read_data.uids,
&mut character_states,
&mut character_activities,
&mut positions,
&mut velocities,
&mut orientations,
@ -133,8 +136,8 @@ impl<'a> System<'a> for Sys {
&mut controllers,
read_data.healths.maybe(),
&read_data.bodies,
&read_data.physics_states,
(
&read_data.physics_states,
read_data.scales.maybe(),
&read_data.stats,
&read_data.skill_sets,
@ -182,6 +185,7 @@ impl<'a> System<'a> for Sys {
entity,
uid,
char_state,
character_activity,
pos,
vel,
ori,
@ -261,6 +265,9 @@ impl Sys {
if *join.char_state != state_update.character {
*join.char_state = state_update.character
}
if *join.character_activity != state_update.character_activity {
*join.character_activity = state_update.character_activity
}
if *join.density != state_update.density {
*join.density = state_update.density
}

View File

@ -2,7 +2,7 @@ use common::{
comp::{
ability::Stance,
agent::{Sound, SoundKind},
Body, BuffChange, ControlEvent, Controller, Pos,
Body, BuffChange, ControlEvent, Controller, Pos, Scale,
},
event::{EventBus, ServerEvent},
uid::UidAllocator,
@ -22,6 +22,7 @@ pub struct ReadData<'a> {
server_bus: Read<'a, EventBus<ServerEvent>>,
positions: ReadStorage<'a, Pos>,
bodies: ReadStorage<'a, Body>,
scales: ReadStorage<'a, Scale>,
}
#[derive(Default)]
@ -91,13 +92,15 @@ impl<'a> System<'a> for Sys {
},
ControlEvent::Respawn => server_emitter.emit(ServerEvent::Respawn(entity)),
ControlEvent::Utterance(kind) => {
if let (Some(pos), Some(body)) = (
if let (Some(pos), Some(body), scale) = (
read_data.positions.get(entity),
read_data.bodies.get(entity),
read_data.scales.get(entity),
) {
let sound = Sound::new(
SoundKind::Utterance(kind, *body),
pos.0 + Vec3::unit_z() * body.eye_height(),
pos.0
+ Vec3::unit_z() * body.eye_height(scale.map_or(1.0, |s| s.0)),
8.0, // TODO: Come up with a better way of determining this
1.0,
);

View File

@ -68,13 +68,14 @@ impl<'a> System<'a> for Sys {
let mut outcomes_emitter = outcomes.emitter();
// Attacks
for (attacker, uid, pos, ori, melee_attack, body) in (
for (attacker, uid, pos, ori, melee_attack, body, scale) in (
&read_data.entities,
&read_data.uids,
&read_data.positions,
&read_data.orientations,
&mut melee_attacks,
&read_data.bodies,
read_data.scales.maybe(),
)
.join()
{
@ -87,7 +88,7 @@ impl<'a> System<'a> for Sys {
melee_attack.applied = true;
// Scales
let eye_pos = pos.0 + Vec3::unit_z() * body.eye_height();
let eye_pos = pos.0 + Vec3::unit_z() * body.eye_height(scale.map_or(1.0, |s| s.0));
let scale = read_data.scales.get(attacker).map_or(1.0, |s| s.0);
let height = body.height() * scale;
// TODO: use Capsule Prisms instead of Cylinders

View File

@ -23,7 +23,7 @@ use common::{
item_drop,
projectile::ProjectileConstructor,
Agent, Alignment, Body, CharacterState, ControlAction, ControlEvent, Controller,
HealthChange, InputKind, InventoryAction, Pos, UnresolvedChatMsg, UtteranceKind,
HealthChange, InputKind, InventoryAction, Pos, Scale, UnresolvedChatMsg, UtteranceKind,
},
effect::{BuffEffect, Effect},
event::{Emitter, ServerEvent},
@ -514,8 +514,10 @@ impl<'a> AgentData<'a> {
target: EcsEntity,
) -> bool {
if let Some(tgt_pos) = read_data.positions.get(target) {
let eye_offset = self.body.map_or(0.0, |b| b.eye_height());
let tgt_eye_offset = read_data.bodies.get(target).map_or(0.0, |b| b.eye_height());
let eye_offset = self.body.map_or(0.0, |b| b.eye_height(self.scale));
let tgt_eye_offset = read_data.bodies.get(target).map_or(0.0, |b| {
b.eye_height(read_data.scales.get(target).map_or(1.0, |s| s.0))
});
if let Some(dir) = Dir::from_unnormalized(
Vec3::new(tgt_pos.0.x, tgt_pos.0.y, tgt_pos.0.z + tgt_eye_offset)
- Vec3::new(self.pos.0.x, self.pos.0.y, self.pos.0.z + eye_offset),
@ -794,8 +796,8 @@ impl<'a> AgentData<'a> {
},
};
let is_detected = |entity: &EcsEntity, e_pos: &Pos| {
self.detects_other(agent, controller, entity, e_pos, read_data)
let is_detected = |entity: &EcsEntity, e_pos: &Pos, e_scale: Option<&Scale>| {
self.detects_other(agent, controller, entity, e_pos, e_scale, read_data)
};
let target = entities_nearby
@ -805,7 +807,7 @@ impl<'a> AgentData<'a> {
.filter_map(|(entity, attack_target)| {
get_pos(entity).map(|pos| (entity, pos, attack_target))
})
.filter(|(entity, e_pos, _)| is_detected(entity, e_pos))
.filter(|(entity, e_pos, _)| is_detected(entity, e_pos, read_data.scales.get(*entity)))
.min_by_key(|(_, e_pos, attack_target)| {
(
*attack_target,
@ -997,9 +999,11 @@ impl<'a> AgentData<'a> {
.angle_between((tgt_data.pos.0 - self.pos.0).xy())
.to_degrees();
let eye_offset = self.body.map_or(0.0, |b| b.eye_height());
let eye_offset = self.body.map_or(0.0, |b| b.eye_height(self.scale));
let tgt_eye_height = tgt_data.body.map_or(0.0, |b| b.eye_height());
let tgt_eye_height = tgt_data
.body
.map_or(0.0, |b| b.eye_height(tgt_data.scale.map_or(1.0, |s| s.0)));
let tgt_eye_offset = tgt_eye_height +
// Special case for jumping attacks to jump at the body
// of the target and not the ground around the target
@ -1037,7 +1041,7 @@ impl<'a> AgentData<'a> {
projectile_speed,
self.pos.0
+ self.body.map_or(Vec3::zero(), |body| {
body.projectile_offsets(self.ori.look_vec())
body.projectile_offsets(self.ori.look_vec(), self.scale)
}),
Vec3::new(
tgt_data.pos.0.x,
@ -1062,7 +1066,7 @@ impl<'a> AgentData<'a> {
projectile_speed,
self.pos.0
+ self.body.map_or(Vec3::zero(), |body| {
body.projectile_offsets(self.ori.look_vec())
body.projectile_offsets(self.ori.look_vec(), self.scale)
}),
Vec3::new(
tgt_data.pos.0.x,
@ -1077,7 +1081,7 @@ impl<'a> AgentData<'a> {
projectile_speed,
self.pos.0
+ self.body.map_or(Vec3::zero(), |body| {
body.projectile_offsets(self.ori.look_vec())
body.projectile_offsets(self.ori.look_vec(), self.scale)
}),
Vec3::new(
tgt_data.pos.0.x,
@ -1684,6 +1688,7 @@ impl<'a> AgentData<'a> {
controller: &Controller,
other: EcsEntity,
other_pos: &Pos,
other_scale: Option<&Scale>,
read_data: &ReadData,
) -> bool {
let other_stealth_multiplier = {
@ -1708,7 +1713,15 @@ impl<'a> AgentData<'a> {
(within_sight_dist)
&& within_fov
&& entities_have_line_of_sight(self.pos, self.body, other_pos, other_body, read_data)
&& entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
other_pos,
other_body,
other_scale,
read_data,
)
}
pub fn detects_other(
@ -1717,10 +1730,11 @@ impl<'a> AgentData<'a> {
controller: &Controller,
other: &EcsEntity,
other_pos: &Pos,
other_scale: Option<&Scale>,
read_data: &ReadData,
) -> bool {
self.can_sense_directly_near(other_pos)
|| self.can_see_entity(agent, controller, *other, other_pos, read_data)
|| self.can_see_entity(agent, controller, *other, other_pos, other_scale, read_data)
}
pub fn can_sense_directly_near(&self, e_pos: &Pos) -> bool {

View File

@ -247,7 +247,15 @@ impl<'a> AgentData<'a> {
read_data: &ReadData,
) {
let line_of_sight_with_target = || {
entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
};
let elevation = self.pos.0.z - tgt_data.pos.0.z;
@ -374,8 +382,10 @@ impl<'a> AgentData<'a> {
&& entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
{
@ -451,8 +461,10 @@ impl<'a> AgentData<'a> {
&& entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
{
@ -1269,7 +1281,15 @@ impl<'a> AgentData<'a> {
const DESIRED_ENERGY_LEVEL: f32 = 50.0;
let line_of_sight_with_target = || {
entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
};
// Logic to use abilities
@ -1521,8 +1541,10 @@ impl<'a> AgentData<'a> {
if entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
) && attack_data.angle < 45.0
{
@ -1574,7 +1596,15 @@ impl<'a> AgentData<'a> {
const DESIRED_COMBO_LEVEL: u32 = 8;
let line_of_sight_with_target = || {
entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
};
// Logic to use abilities
@ -1724,8 +1754,10 @@ impl<'a> AgentData<'a> {
) && entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
) && attack_data.angle < 90.0
{
@ -1907,8 +1939,10 @@ impl<'a> AgentData<'a> {
&& entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
{
@ -2130,8 +2164,10 @@ impl<'a> AgentData<'a> {
&& entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
{
@ -2365,8 +2401,15 @@ impl<'a> AgentData<'a> {
tgt_data: &TargetData,
read_data: &ReadData,
) {
if entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
&& attack_data.angle < 15.0
if entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
) && attack_data.angle < 15.0
{
controller.push_basic_input(InputKind::Primary);
} else {
@ -2383,8 +2426,15 @@ impl<'a> AgentData<'a> {
read_data: &ReadData,
) {
controller.inputs.look_dir = self.ori.look_dir();
if entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
&& attack_data.angle < 15.0
if entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
) && attack_data.angle < 15.0
{
controller.push_basic_input(InputKind::Primary);
} else {
@ -2406,8 +2456,15 @@ impl<'a> AgentData<'a> {
.try_normalized()
.unwrap_or_default(),
);
if entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
{
if entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
) {
controller.push_basic_input(InputKind::Primary);
} else {
agent.target = None;
@ -2467,8 +2524,10 @@ impl<'a> AgentData<'a> {
if entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
) {
// If close to target, use either primary or secondary ability
@ -2564,8 +2623,10 @@ impl<'a> AgentData<'a> {
&& entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
&& attack_data.angle < 15.0
@ -2695,8 +2756,10 @@ impl<'a> AgentData<'a> {
&& entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
&& attack_data.angle < 15.0
@ -2931,8 +2994,10 @@ impl<'a> AgentData<'a> {
&& entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
{
@ -3186,7 +3251,15 @@ impl<'a> AgentData<'a> {
.and_then(|e| read_data.velocities.get(e))
.map_or(0.0, |v| v.0.cross(self.ori.look_vec()).magnitude_squared());
let line_of_sight_with_target = || {
entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
};
if attack_data.dist_sqrd < golem_melee_range.powi(2) {
@ -3263,7 +3336,15 @@ impl<'a> AgentData<'a> {
let health_fraction = self.health.map_or(0.5, |h| h.fraction());
let line_of_sight_with_target = || {
entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
};
// Sets counter at start of combat, using `condition` to keep track of whether
@ -3461,7 +3542,15 @@ impl<'a> AgentData<'a> {
let health_fraction = self.health.map_or(0.5, |h| h.fraction());
let line_of_sight_with_target = || {
entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
};
if health_fraction < VINE_CREATION_THRESHOLD
@ -3530,7 +3619,15 @@ impl<'a> AgentData<'a> {
}
let line_of_sight_with_target = || {
entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
};
let health_fraction = self.health.map_or(0.5, |h| h.fraction());
// Sets counter at start of combat, using `condition` to keep track of whether
@ -3671,8 +3768,10 @@ impl<'a> AgentData<'a> {
&& entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
{
@ -3738,8 +3837,10 @@ impl<'a> AgentData<'a> {
&& entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
{
@ -3822,8 +3923,10 @@ impl<'a> AgentData<'a> {
if entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
) && attack_data.angle < 45.0
{
@ -3901,8 +4004,10 @@ impl<'a> AgentData<'a> {
} else if entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
) {
// if enemy in mid range shoot dagon bombs and steamwave
@ -4016,8 +4121,10 @@ impl<'a> AgentData<'a> {
&& entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
{
@ -4177,8 +4284,10 @@ impl<'a> AgentData<'a> {
} else if entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
) {
// Else if in sight, barrage
@ -4241,8 +4350,10 @@ impl<'a> AgentData<'a> {
&& entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
&& agent.action_state.timers[DASH_TIMER] > 4.0

View File

@ -2,7 +2,7 @@ use crate::data::{ActionMode, AgentData, AttackData, Path, ReadData, TargetData}
use common::{
comp::{
agent::Psyche, buff::BuffKind, inventory::item::ItemTag, item::ItemDesc, Agent, Alignment,
Body, Controller, InputKind, Pos,
Body, Controller, InputKind, Pos, Scale,
},
consts::GRAVITY,
terrain::Block,
@ -146,17 +146,19 @@ pub fn are_our_owners_hostile(
pub fn entities_have_line_of_sight(
pos: &Pos,
body: Option<&Body>,
scale: f32,
other_pos: &Pos,
other_body: Option<&Body>,
other_scale: Option<&Scale>,
read_data: &ReadData,
) -> bool {
let get_eye_pos = |pos: &Pos, body: Option<&Body>| {
let eye_offset = body.map_or(0.0, |b| b.eye_height());
let get_eye_pos = |pos: &Pos, body: Option<&Body>, scale: f32| {
let eye_offset = body.map_or(0.0, |b| b.eye_height(scale));
Pos(pos.0.with_z(pos.0.z + eye_offset))
};
let eye_pos = get_eye_pos(pos, body);
let other_eye_pos = get_eye_pos(other_pos, other_body);
let eye_pos = get_eye_pos(pos, body, scale);
let other_eye_pos = get_eye_pos(other_pos, other_body, other_scale.map_or(1.0, |s| s.0));
positions_have_line_of_sight(&eye_pos, &other_eye_pos, read_data)
}

View File

@ -286,6 +286,7 @@ impl StateExt for State {
.with(poise)
.with(comp::Alignment::Npc)
.with(comp::CharacterState::default())
.with(comp::CharacterActivity::default())
.with(inventory)
.with(comp::Buffs::default())
.with(comp::Combo::default())
@ -352,6 +353,7 @@ impl StateExt for State {
.with(comp::Controller::default())
.with(Inventory::with_empty())
.with(comp::CharacterState::default())
.with(comp::CharacterActivity::default())
// TODO: some of these are required in order for the character_behavior system to
// recognize a possesed airship; that system should be refactored to use `.maybe()`
.with(comp::Energy::new(ship.into(), 0))
@ -559,6 +561,7 @@ impl StateExt for State {
z_max: 1.75,
});
self.write_component_ignore_entity_dead(entity, comp::CharacterState::default());
self.write_component_ignore_entity_dead(entity, comp::CharacterActivity::default());
self.write_component_ignore_entity_dead(entity, comp::Alignment::Owned(player_uid));
self.write_component_ignore_entity_dead(entity, comp::Buffs::default());
self.write_component_ignore_entity_dead(entity, comp::Auras::default());

View File

@ -139,11 +139,12 @@ impl BehaviorTree {
handle_inbox_trade_accepted,
handle_inbox_finished_trade,
handle_inbox_update_pending_trade,
handle_timed_events,
]);
Self { tree }
} else {
Self {
tree: vec![handle_inbox_cancel_interactions],
tree: vec![handle_inbox_cancel_interactions, handle_timed_events],
}
}
}
@ -477,7 +478,6 @@ fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool {
if bdata.agent.allowed_to_speak()
&& let Some(target) = bdata.read_data.lookup_actor(actor)
&& let Some(target_pos) = bdata.read_data.positions.get(target)
&& bdata.agent_data.look_toward(bdata.controller, bdata.read_data, target)
{
bdata.agent.target = Some(Target::new(
target,
@ -486,6 +486,8 @@ fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool {
false,
Some(target_pos.0),
));
// We're always aware of someone we're talking to
bdata.agent.awareness.set_maximally_aware();
bdata.controller.push_action(ControlAction::Talk);
bdata.controller.push_utterance(UtteranceKind::Greeting);
@ -497,15 +499,20 @@ fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool {
.agent
.timer
.start(bdata.read_data.time.0, TimerAction::Interact);
true
} else {
false
}
},
NpcAction::Say(msg) => {
bdata.controller.push_utterance(UtteranceKind::Greeting);
bdata.agent_data.chat_npc(msg, bdata.event_emitter);
false
},
}
}
} else {
false
}
}
/// Handle timed events, like looking at the player we are talking to
@ -571,7 +578,14 @@ fn update_last_known_pos(bdata: &mut BehaviorData) -> bool {
let target = target_info.target;
if let Some(target_pos) = read_data.positions.get(target) {
if agent_data.detects_other(agent, controller, &target, target_pos, read_data) {
if agent_data.detects_other(
agent,
controller,
&target,
target_pos,
read_data.scales.get(target),
read_data,
) {
let updated_pos = Some(target_pos.0);
let Target {
@ -631,9 +645,10 @@ fn update_target_awareness(bdata: &mut BehaviorData) -> bool {
let target = agent.target.map(|t| t.target);
let tgt_pos = target.and_then(|t| read_data.positions.get(t));
let tgt_scale = target.and_then(|t| read_data.scales.get(t));
if let (Some(target), Some(tgt_pos)) = (target, tgt_pos) {
if agent_data.can_see_entity(agent, controller, target, tgt_pos, read_data) {
if agent_data.can_see_entity(agent, controller, target, tgt_pos, tgt_scale, read_data) {
agent.awareness.change_by(1.75 * read_data.dt.0);
} else if agent_data.can_sense_directly_near(tgt_pos) {
agent.awareness.change_by(0.25);

View File

@ -97,8 +97,9 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool {
false,
target_pos,
));
// We're always aware of someone we're talking to
agent.awareness.set_maximally_aware();
if agent_data.look_toward(controller, read_data, target) {
controller.push_action(ControlAction::Stand);
controller.push_action(ControlAction::Talk);
controller.push_utterance(UtteranceKind::Greeting);
@ -111,8 +112,8 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool {
let standard_response_msg = || -> String {
if personality.will_ambush() {
format!(
"I'm heading to {}! Want to come along? We'll \
make great travel buddies, hehe.",
"I'm heading to {}! Want to come along? We'll make \
great travel buddies, hehe.",
destination_name
)
} else if personality.is(PersonalityTrait::Extroverted) {
@ -135,17 +136,15 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool {
if personality.is(PersonalityTrait::Extroverted) {
format!(
"Greetings fair {}! It has been far too long \
since last I saw you. I'm going to {} right \
now.",
since last I saw you. I'm going to {} right now.",
&tgt_stats.name, destination_name
)
} else if personality.is(PersonalityTrait::Disagreeable)
{
} else if personality.is(PersonalityTrait::Disagreeable) {
"Oh. It's you again.".to_string()
} else {
format!(
"Hi again {}! Unfortunately I'm in a hurry \
right now. See you!",
"Hi again {}! Unfortunately I'm in a hurry right \
now. See you!",
&tgt_stats.name
)
}
@ -166,9 +165,7 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool {
PersonalityTrait::Adventurous => {
"npc-speech-villager_adventurous"
},
PersonalityTrait::Closed => {
"npc-speech-villager_closed"
},
PersonalityTrait::Closed => "npc-speech-villager_closed",
PersonalityTrait::Conscientious => {
"npc-speech-villager_conscientious"
},
@ -196,18 +193,12 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool {
PersonalityTrait::Neurotic => {
"npc-speech-villager_neurotic"
},
PersonalityTrait::Seeker => {
"npc-speech-villager_seeker"
},
PersonalityTrait::Seeker => "npc-speech-villager_seeker",
PersonalityTrait::SadLoner => {
"npc-speech-villager_sad_loner"
},
PersonalityTrait::Worried => {
"npc-speech-villager_worried"
},
PersonalityTrait::Stable => {
"npc-speech-villager_stable"
},
PersonalityTrait::Worried => "npc-speech-villager_worried",
PersonalityTrait::Stable => "npc-speech-villager_stable",
};
agent_data.chat_npc(msg, event_emitter);
} else {
@ -261,24 +252,21 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool {
Subject::Person(person) => {
if let Some(src_pos) = read_data.positions.get(target) {
let msg = if let Some(person_pos) = person.origin {
let distance =
Distance::from_dir(person_pos.xy() - src_pos.0.xy());
let distance = Distance::from_dir(person_pos.xy() - src_pos.0.xy());
match distance {
Distance::NextTo | Distance::Near => {
format!(
"{} ? I think he's {} {} from here!",
person.name(),
distance.name(),
Direction::from_dir(
person_pos.xy() - src_pos.0.xy(),
)
Direction::from_dir(person_pos.xy() - src_pos.0.xy(),)
.name()
)
},
_ => {
format!(
"{} ? I think he's gone visiting another town. \
Come back later!",
"{} ? I think he's gone visiting another town. Come \
back later!",
person.name()
)
},
@ -297,7 +285,6 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool {
}
}
}
}
true
}

View File

@ -31,7 +31,8 @@ impl Animation for TalkAnimation {
let slowb = (anim_time * 4.0 + PI / 2.0).sin();
let slowc = (anim_time * 12.0 + PI / 2.0).sin();
next.head.orientation = Quaternion::rotation_x(slowc * 0.035 + look_dir.z * 0.7);
next.head.orientation =
Quaternion::rotation_x(slowc * 0.035 + look_dir.z.atan2(look_dir.xy().magnitude()));
next.hand_l.position = Vec3::new(
-s_a.hand.0 + 0.5 + slowb * 0.5,
s_a.hand.1 + 5.0 + slowc * 1.0,

View File

@ -346,8 +346,19 @@ pub fn maintain_egui_inner(
ui.label("Body");
ui.label("Poise");
ui.label("Character State");
ui.label("Character Activity");
ui.end_row();
for (entity, body, stats, pos, _ori, vel, poise, character_state) in (
for (
entity,
body,
stats,
pos,
_ori,
vel,
poise,
character_state,
character_activity,
) in (
&ecs.entities(),
ecs.read_storage::<Body>().maybe(),
ecs.read_storage::<comp::Stats>().maybe(),
@ -356,14 +367,18 @@ pub fn maintain_egui_inner(
ecs.read_storage::<comp::Vel>().maybe(),
ecs.read_storage::<Poise>().maybe(),
ecs.read_storage::<comp::CharacterState>().maybe(),
ecs.read_storage::<comp::CharacterActivity>().maybe(),
)
.join()
.filter(|(_, _, _, pos, _, _, _, _)| {
.filter(
|(_, _, _, pos, _, _, _, _, _)| {
client_pos.map_or(true, |client_pos| {
pos.map_or(0.0, |pos| pos.0.distance_squared(client_pos.0))
< max_entity_distance
})
pos.map_or(0.0, |pos| {
pos.0.distance_squared(client_pos.0)
}) < max_entity_distance
})
},
)
{
if ui.button("View").clicked() {
previous_selected_entity =
@ -420,6 +435,12 @@ pub fn maintain_egui_inner(
ui.label("-");
}
if let Some(character_activity) = character_activity {
ui.label(format!("{:?}", character_activity));
} else {
ui.label("-");
}
ui.end_row();
}
});
@ -507,11 +528,11 @@ fn selected_entity_window(
buffs,
auras,
character_state,
character_activity,
physics_state,
alignment,
scale,
mass,
(density, health, energy),
(mass, density, health, energy),
) in (
&ecs.entities(),
ecs.read_storage::<Body>().maybe(),
@ -523,18 +544,19 @@ fn selected_entity_window(
ecs.read_storage::<comp::Buffs>().maybe(),
ecs.read_storage::<comp::Auras>().maybe(),
ecs.read_storage::<comp::CharacterState>().maybe(),
ecs.read_storage::<comp::CharacterActivity>().maybe(),
ecs.read_storage::<comp::PhysicsState>().maybe(),
ecs.read_storage::<comp::Alignment>().maybe(),
ecs.read_storage::<comp::Scale>().maybe(),
ecs.read_storage::<comp::Mass>().maybe(),
(
ecs.read_storage::<comp::Mass>().maybe(),
ecs.read_storage::<comp::Density>().maybe(),
ecs.read_storage::<comp::Health>().maybe(),
ecs.read_storage::<comp::Energy>().maybe(),
),
)
.join()
.filter(|(e, _, _, _, _, _, _, _, _, _, _, _, _, _, (_, _, _))| e.id() == entity_id)
.filter(|(e, _, _, _, _, _, _, _, _, _, _, _, _, _, (_, _, _, _))| e.id() == entity_id)
{
let time = ecs.read_resource::<Time>();
if let Some(pos) = pos {
@ -702,6 +724,18 @@ fn selected_entity_window(
});
}
if let Some(character_activity) = character_activity {
CollapsingHeader::new("CharacterActivity").default_open(false).show(ui, |ui| {
Grid::new("selected_entity_character_activity_grid")
.spacing([40.0, 4.0])
.max_col_width(100.0)
.striped(true)
.show(ui, |ui| {
two_col_row(ui, "look_dir", format!("{:.3?}", character_activity.look_dir));
});
});
}
if let Some(physics_state) = physics_state {
CollapsingHeader::new("Physics State").default_open(false).show(ui, |ui| {
Grid::new("selected_entity_physics_state_grid")

View File

@ -33,9 +33,9 @@ use common::{
comp::{
inventory::slot::EquipSlot,
item::{tool::AbilityContext, Hands, ItemKind, ToolKind},
ship, Body, CharacterState, Collider, Controller, Health, Inventory, Item, ItemKey, Last,
LightAnimation, LightEmitter, Ori, PhysicsState, PoiseState, Pos, Scale, SkillSet, Stance,
Vel,
ship, Body, CharacterActivity, CharacterState, Collider, Controller, Health, Inventory,
Item, ItemKey, Last, LightAnimation, LightEmitter, Ori, PhysicsState, PoiseState, Pos,
Scale, SkillSet, Stance, Vel,
},
link::Is,
mounting::Rider,
@ -741,14 +741,14 @@ impl FigureMgr {
scale,
body,
character,
character_activity,
last_character,
physics,
health,
inventory,
item,
light_emitter,
is_rider,
(collider, stance, skillset),
(is_rider, collider, stance, skillset),
),
) in (
&ecs.entities(),
@ -759,14 +759,15 @@ impl FigureMgr {
ecs.read_storage::<Scale>().maybe(),
&ecs.read_storage::<Body>(),
ecs.read_storage::<CharacterState>().maybe(),
ecs.read_storage::<CharacterActivity>().maybe(),
ecs.read_storage::<Last<CharacterState>>().maybe(),
&ecs.read_storage::<PhysicsState>(),
ecs.read_storage::<Health>().maybe(),
ecs.read_storage::<Inventory>().maybe(),
ecs.read_storage::<Item>().maybe(),
ecs.read_storage::<LightEmitter>().maybe(),
ecs.read_storage::<Is<Rider>>().maybe(),
(
ecs.read_storage::<Is<Rider>>().maybe(),
ecs.read_storage::<Collider>().maybe(),
ecs.read_storage::<Stance>().maybe(),
ecs.read_storage::<SkillSet>().maybe(),
@ -779,7 +780,13 @@ impl FigureMgr {
let rel_vel = anim::vek::Vec3::<f32>::from(vel.0 - physics.ground_vel)
/ scale.map_or(1.0, |s| s.0);
let look_dir = controller.map(|c| c.inputs.look_dir).unwrap_or_default();
// Priortise CharacterActivity as the source of the look direction
let look_dir = character_activity.and_then(|ca| ca.look_dir)
// Failing that, take the controller as the source of truth
.or_else(|| controller.map(|c| c.inputs.look_dir))
// If that still didn't work, fall back to the interpolation orientation
.or_else(|| interpolated.map(|i| i.ori.look_dir()))
.unwrap_or_default();
let is_viewpoint = scene_data.viewpoint_entity == entity;
let viewpoint_camera_mode = if is_viewpoint {
camera_mode

View File

@ -547,7 +547,7 @@ impl Scene {
(
matches!(b, comp::Body::Humanoid(_)),
b.height(),
b.eye_height(),
b.eye_height(1.0), // Scale is applied later
)
});

View File

@ -12,7 +12,7 @@ use common::{
assets::{AssetExt, DotVoxAsset},
comp::{
self, aura, beam, body, buff, item::Reagent, object, shockwave, BeamSegment, Body,
CharacterState, Ori, Pos, Shockwave, Vel,
CharacterState, Ori, Pos, Scale, Shockwave, Vel,
},
figure::Segment,
outcome::Outcome,
@ -1264,12 +1264,13 @@ impl ParticleMgr {
let time = state.get_time();
let mut rng = thread_rng();
for (interp, pos, buffs, body, ori) in (
for (interp, pos, buffs, body, ori, scale) in (
ecs.read_storage::<Interpolated>().maybe(),
&ecs.read_storage::<Pos>(),
&ecs.read_storage::<comp::Buffs>(),
&ecs.read_storage::<Body>(),
&ecs.read_storage::<Ori>(),
ecs.read_storage::<Scale>().maybe(),
)
.join()
{
@ -1328,7 +1329,8 @@ impl ParticleMgr {
self.scheduler.heartbeats(Duration::from_millis(25)),
),
|| {
let start_pos = pos + Vec3::unit_z() * body.eye_height();
let start_pos = pos
+ Vec3::unit_z() * body.eye_height(scale.map_or(1.0, |s| s.0));
let (radius, theta) =
(rng.gen_range(0.0f32..1.0).sqrt(), rng.gen_range(0.0..TAU));
let end_pos = pos
@ -1393,7 +1395,9 @@ impl ParticleMgr {
+ multiplicity
* self.scheduler.heartbeats(Duration::from_millis(3)) as usize,
|| {
let start_pos = pos + Vec3::unit_z() * body.eye_height() / 2.0;
let start_pos = pos
+ Vec3::unit_z() * body.eye_height(scale.map_or(1.0, |s| s.0))
/ 2.0;
let end_pos = start_pos
+ Vec3::<f32>::zero()
.map(|_| rng.gen_range(-1.0..1.0))