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::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_actors.maybe(),
70 (
71 read_data.is_mounts.maybe(),
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_actor,
97 (is_mount, is_rider, is_volume_rider),
98 )| {
99 let mut emitters = events.get_emitters();
100 let mut rng = rng();
101
102 if is_mount.is_some() && body.is_none_or(|body| !body.has_free_will()) {
103 return;
104 }
105
106 // The entity that is moving, if riding it's the mount, otherwise it's itself
107 let moving_entity = is_rider
108 .and_then(|is_rider| {
109 let mut mount = is_rider.mount;
110 // Find the root mount, i.e the one that's doing the moving.
111 loop {
112 let e = read_data.id_maps.uid_entity(mount)?;
113
114 if let Some(is_rider) = read_data.is_riders.get(e) {
115 mount = is_rider.mount;
116 } else {
117 return Some(e);
118 }
119 }
120 })
121 .or_else(|| {
122 is_volume_rider.and_then(|is_volume_rider| {
123 match is_volume_rider.pos.kind {
124 Volume::Terrain => None,
125 Volume::Entity(uid) => read_data.id_maps.uid_entity(uid),
126 }
127 })
128 })
129 .unwrap_or(entity);
130
131 let pos = read_data.positions.get(moving_entity).unwrap_or(pos);
132 let vel = read_data.velocities.get(moving_entity).unwrap_or(vel);
133 let moving_body = read_data.bodies.get(moving_entity);
134 let physics_state = read_data
135 .physics_states
136 .get(moving_entity)
137 .unwrap_or(physics_state);
138
139 // Hack, replace with better system when groups are more sophisticated
140 // Override alignment if in a group unless entity is owned already
141 let alignment = if matches!(
142 &read_data.alignments.get(entity),
143 &Some(Alignment::Owned(_))
144 ) {
145 read_data.alignments.get(entity).copied()
146 } else {
147 group
148 .and_then(|g| read_data.group_manager.group_info(*g))
149 .and_then(|info| read_data.uids.get(info.leader))
150 .copied()
151 .map_or_else(
152 || read_data.alignments.get(entity).copied(),
153 |uid| Some(Alignment::Owned(uid)),
154 )
155 };
156
157 if !matches!(
158 char_state,
159 CharacterState::LeapMelee(_) | CharacterState::Glide(_)
160 ) {
161 // Default to looking in orientation direction
162 // (can be overridden below)
163 //
164 // This definitely breaks LeapMelee, Glide and
165 // probably not only that, do we really need this at all?
166 controller.reset();
167 controller.inputs.look_dir = ori.look_dir();
168 }
169
170 let scale = read_data
171 .scales
172 .get(moving_entity)
173 .map_or(1.0, |Scale(s)| *s);
174
175 let glider_equipped = inventory
176 .equipped(EquipSlot::Glider)
177 .as_ref()
178 .is_some_and(|item| matches!(&*item.kind(), comp::item::ItemKind::Glider));
179
180 let is_gliding = matches!(
181 read_data.char_states.get(entity),
182 Some(CharacterState::GlideWield(_) | CharacterState::Glide(_))
183 ) && physics_state.on_ground.is_none();
184
185 // This controls how picky NPCs are about their pathfinding.
186 // Giants are larger and so can afford to be less precise
187 // when trying to move around the world
188 // (especially since they would otherwise get stuck on
189 // obstacles that smaller entities would not).
190 let node_tolerance = scale * 1.5;
191 let slow_factor =
192 moving_body.map_or(0.0, |b| 1.0 - 1.0 / (1.0 + b.base_accel() * 0.01));
193 let traversal_config = TraversalConfig {
194 node_tolerance,
195 slow_factor,
196 on_ground: physics_state.on_ground.is_some(),
197 in_liquid: physics_state.in_liquid().is_some(),
198 min_tgt_dist: scale * moving_body.map_or(1.0, |body| body.max_radius()),
199 can_climb: moving_body.is_some_and(Body::can_climb),
200 can_fly: moving_body.is_some_and(|b| b.fly_thrust().is_some()),
201 vectored_propulsion: moving_body.is_some_and(|b| b.vectored_propulsion()),
202 is_target_loaded: true,
203 };
204 let health_fraction = health.map_or(1.0, Health::fraction);
205
206 // Package all this agent's data into a convenient struct
207 let data = AgentData {
208 entity: &entity,
209 rtsim_actor,
210 uid,
211 pos,
212 vel,
213 ori,
214 energy,
215 body,
216 inventory,
217 skill_set,
218 physics_state,
219 alignment: alignment.as_ref(),
220 traversal_config,
221 scale,
222 damage: health_fraction,
223 light_emitter,
224 glider_equipped,
225 is_gliding,
226 health: read_data.healths.get(entity),
227 heads: read_data.heads.get(entity),
228 char_state,
229 active_abilities,
230 combo,
231 buffs: read_data.buffs.get(entity),
232 stats: read_data.stats.get(entity),
233 cached_spatial_grid: &read_data.cached_spatial_grid,
234 msm: &read_data.msm,
235 poise: read_data.poises.get(entity),
236 stance: read_data.stances.get(entity),
237 };
238
239 ///////////////////////////////////////////////////////////
240 // Behavior tree
241 ///////////////////////////////////////////////////////////
242 // The behavior tree is meant to make decisions for agents
243 // *but should not* mutate any data (only action nodes
244 // should do that). Each path should lead to one (and only
245 // one) action node. This makes bugfinding much easier and
246 // debugging way easier. If you don't think so, try
247 // debugging the agent code before this MR
248 // (https://gitlab.com/veloren/veloren/-/merge_requests/1801).
249 // Each tick should arrive at one (1) action node which
250 // then determines what the agent does. If this makes you
251 // uncomfortable, consider dt the response time of the
252 // NPC. To make the tree easier to read, subtrees can be
253 // created as methods on `AgentData`. Action nodes are
254 // also methods on the `AgentData` struct. Action nodes
255 // are the only parts of this tree that should provide
256 // inputs.
257 let mut behavior_data = BehaviorData {
258 agent,
259 agent_data: data,
260 read_data: &read_data,
261 emitters: &mut emitters,
262 controller,
263 rng: &mut rng,
264 };
265
266 BehaviorTree::root().run(&mut behavior_data);
267
268 debug_assert!(controller.inputs.move_dir.map(|e| !e.is_nan()).reduce_and());
269 debug_assert!(controller.inputs.look_dir.map(|e| !e.is_nan()).reduce_and());
270 },
271 );
272 }
273}