veloren_server/sys/agent/mod.rs
1pub mod behavior_tree;
2use server_agent::data::AgentEvents;
3pub use server_agent::{action_nodes, attack, consts, data, util};
4
5use crate::sys::agent::{
6 behavior_tree::{BehaviorData, BehaviorTree},
7 data::{AgentData, ReadData},
8};
9use common::{
10 comp::{
11 self, Agent, Alignment, Body, CharacterState, Controller, Health, Scale,
12 inventory::slot::EquipSlot, item::ItemDesc,
13 },
14 mounting::Volume,
15 path::TraversalConfig,
16};
17use common_base::prof_span;
18use common_ecs::{Job, Origin, ParMode, Phase, System};
19use rand::thread_rng;
20use rayon::iter::ParallelIterator;
21use specs::{LendJoin, ParJoin, WriteStorage};
22
23/// This system will allow NPCs to modify their controller
24#[derive(Default)]
25pub struct Sys;
26impl<'a> System<'a> for Sys {
27 type SystemData = (
28 ReadData<'a>,
29 AgentEvents<'a>,
30 WriteStorage<'a, Agent>,
31 WriteStorage<'a, Controller>,
32 );
33
34 const NAME: &'static str = "agent";
35 const ORIGIN: Origin = Origin::Server;
36 const PHASE: Phase = Phase::Create;
37
38 fn run(
39 job: &mut Job<Self>,
40 (read_data, events, mut agents, mut controllers): Self::SystemData,
41 ) {
42 job.cpu_stats.measure(ParMode::Rayon);
43
44 (
45 &read_data.entities,
46 (
47 &read_data.energies,
48 read_data.healths.maybe(),
49 read_data.combos.maybe(),
50 ),
51 (
52 &read_data.positions,
53 &read_data.velocities,
54 &read_data.orientations,
55 ),
56 read_data.bodies.maybe(),
57 &read_data.inventories,
58 (
59 &read_data.char_states,
60 &read_data.skill_set,
61 &read_data.active_abilities,
62 ),
63 &read_data.physics_states,
64 &read_data.uids,
65 &mut agents,
66 &mut controllers,
67 read_data.light_emitter.maybe(),
68 read_data.groups.maybe(),
69 read_data.rtsim_entities.maybe(),
70 (
71 !&read_data.is_mounts,
72 read_data.is_riders.maybe(),
73 read_data.is_volume_riders.maybe(),
74 ),
75 )
76 .par_join()
77 .for_each_init(
78 || {
79 prof_span!(guard, "agent rayon job");
80 guard
81 },
82 |_guard,
83 (
84 entity,
85 (energy, health, combo),
86 (pos, vel, ori),
87 body,
88 inventory,
89 (char_state, skill_set, active_abilities),
90 physics_state,
91 uid,
92 agent,
93 controller,
94 light_emitter,
95 group,
96 rtsim_entity,
97 (_, is_rider, is_volume_rider),
98 )| {
99 let mut emitters = events.get_emitters();
100 let mut rng = thread_rng();
101
102 // The entity that is moving, if riding it's the mount, otherwise it's itself
103 let moving_entity = is_rider
104 .and_then(|is_rider| {
105 let mut mount = is_rider.mount;
106 // Find the root mount, i.e the one that's doing the moving.
107 loop {
108 let e = read_data.id_maps.uid_entity(mount)?;
109
110 if let Some(is_rider) = read_data.is_riders.get(e) {
111 mount = is_rider.mount;
112 } else {
113 return Some(e);
114 }
115 }
116 })
117 .or_else(|| {
118 is_volume_rider.and_then(|is_volume_rider| {
119 match is_volume_rider.pos.kind {
120 Volume::Terrain => None,
121 Volume::Entity(uid) => read_data.id_maps.uid_entity(uid),
122 }
123 })
124 })
125 .unwrap_or(entity);
126
127 let pos = read_data.positions.get(moving_entity).unwrap_or(pos);
128 let vel = read_data.velocities.get(moving_entity).unwrap_or(vel);
129 let moving_body = read_data.bodies.get(moving_entity);
130 let physics_state = read_data
131 .physics_states
132 .get(moving_entity)
133 .unwrap_or(physics_state);
134
135 // Hack, replace with better system when groups are more sophisticated
136 // Override alignment if in a group unless entity is owned already
137 let alignment = if matches!(
138 &read_data.alignments.get(entity),
139 &Some(Alignment::Owned(_))
140 ) {
141 read_data.alignments.get(entity).copied()
142 } else {
143 group
144 .and_then(|g| read_data.group_manager.group_info(*g))
145 .and_then(|info| read_data.uids.get(info.leader))
146 .copied()
147 .map_or_else(
148 || read_data.alignments.get(entity).copied(),
149 |uid| Some(Alignment::Owned(uid)),
150 )
151 };
152
153 if !matches!(
154 char_state,
155 CharacterState::LeapMelee(_) | CharacterState::Glide(_)
156 ) {
157 // Default to looking in orientation direction
158 // (can be overridden below)
159 //
160 // This definitely breaks LeapMelee, Glide and
161 // probably not only that, do we really need this at all?
162 controller.reset();
163 controller.inputs.look_dir = ori.look_dir();
164 }
165
166 let scale = read_data
167 .scales
168 .get(moving_entity)
169 .map_or(1.0, |Scale(s)| *s);
170
171 let glider_equipped = inventory
172 .equipped(EquipSlot::Glider)
173 .as_ref()
174 .is_some_and(|item| matches!(&*item.kind(), comp::item::ItemKind::Glider));
175
176 let is_gliding = matches!(
177 read_data.char_states.get(entity),
178 Some(CharacterState::GlideWield(_) | CharacterState::Glide(_))
179 ) && physics_state.on_ground.is_none();
180
181 // This controls how picky NPCs are about their pathfinding.
182 // Giants are larger and so can afford to be less precise
183 // when trying to move around the world
184 // (especially since they would otherwise get stuck on
185 // obstacles that smaller entities would not).
186 let node_tolerance = scale * 1.5;
187 let slow_factor =
188 moving_body.map_or(0.0, |b| 1.0 - 1.0 / (1.0 + b.base_accel() * 0.01));
189 let traversal_config = TraversalConfig {
190 node_tolerance,
191 slow_factor,
192 on_ground: physics_state.on_ground.is_some(),
193 in_liquid: physics_state.in_liquid().is_some(),
194 min_tgt_dist: scale * moving_body.map_or(1.0, |body| body.max_radius()),
195 can_climb: moving_body.is_some_and(Body::can_climb),
196 can_fly: moving_body.is_some_and(|b| b.fly_thrust().is_some()),
197 vectored_propulsion: moving_body.is_some_and(|b| b.vectored_propulsion()),
198 is_target_loaded: true,
199 };
200 let health_fraction = health.map_or(1.0, Health::fraction);
201
202 // Package all this agent's data into a convenient struct
203 let data = AgentData {
204 entity: &entity,
205 rtsim_entity,
206 uid,
207 pos,
208 vel,
209 ori,
210 energy,
211 body,
212 inventory,
213 skill_set,
214 physics_state,
215 alignment: alignment.as_ref(),
216 traversal_config,
217 scale,
218 damage: health_fraction,
219 light_emitter,
220 glider_equipped,
221 is_gliding,
222 health: read_data.healths.get(entity),
223 heads: read_data.heads.get(entity),
224 char_state,
225 active_abilities,
226 combo,
227 buffs: read_data.buffs.get(entity),
228 stats: read_data.stats.get(entity),
229 cached_spatial_grid: &read_data.cached_spatial_grid,
230 msm: &read_data.msm,
231 poise: read_data.poises.get(entity),
232 stance: read_data.stances.get(entity),
233 };
234
235 ///////////////////////////////////////////////////////////
236 // Behavior tree
237 ///////////////////////////////////////////////////////////
238 // The behavior tree is meant to make decisions for agents
239 // *but should not* mutate any data (only action nodes
240 // should do that). Each path should lead to one (and only
241 // one) action node. This makes bugfinding much easier and
242 // debugging way easier. If you don't think so, try
243 // debugging the agent code before this MR
244 // (https://gitlab.com/veloren/veloren/-/merge_requests/1801).
245 // Each tick should arrive at one (1) action node which
246 // then determines what the agent does. If this makes you
247 // uncomfortable, consider dt the response time of the
248 // NPC. To make the tree easier to read, subtrees can be
249 // created as methods on `AgentData`. Action nodes are
250 // also methods on the `AgentData` struct. Action nodes
251 // are the only parts of this tree that should provide
252 // inputs.
253 let mut behavior_data = BehaviorData {
254 agent,
255 agent_data: data,
256 read_data: &read_data,
257 emitters: &mut emitters,
258 controller,
259 rng: &mut rng,
260 };
261
262 BehaviorTree::root().run(&mut behavior_data);
263
264 debug_assert!(controller.inputs.move_dir.map(|e| !e.is_nan()).reduce_and());
265 debug_assert!(controller.inputs.look_dir.map(|e| !e.is_nan()).reduce_and());
266 },
267 );
268 }
269}