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}