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