veloren_common_systems/
character_behavior.rs

1use common_net::synced_components::Heads;
2use specs::{
3    Entities, LazyUpdate, LendJoin, Read, ReadExpect, ReadStorage, SystemData, WriteStorage, shred,
4};
5
6use common::{
7    comp::{
8        self, ActiveAbilities, Beam, Body, CharacterActivity, CharacterState, Combo, Controller,
9        Density, Energy, Health, Inventory, InventoryManip, Mass, Melee, Ori, PhysicsState, Poise,
10        Pos, PreviousPhysCache, Scale, SkillSet, Stance, StateUpdate, Stats, Vel,
11        character_state::{CharacterStateEvents, OutputEvents},
12        inventory::item::{MaterialStatManifest, tool::AbilityMap},
13    },
14    event::{self, EventBus, KnockbackEvent, LocalEvent},
15    link::Is,
16    mounting::{Rider, VolumeRider},
17    outcome::Outcome,
18    resources::{DeltaTime, Time},
19    states::{
20        behavior::{JoinData, JoinStruct},
21        idle,
22    },
23    terrain::TerrainGrid,
24    uid::{IdMaps, Uid},
25};
26use common_ecs::{Job, Origin, Phase, System};
27
28#[derive(SystemData)]
29pub struct ReadData<'a> {
30    entities: Entities<'a>,
31    events: CharacterStateEvents<'a>,
32    local_bus: Read<'a, EventBus<LocalEvent>>,
33    dt: Read<'a, DeltaTime>,
34    time: Read<'a, Time>,
35    lazy_update: Read<'a, LazyUpdate>,
36    healths: ReadStorage<'a, Health>,
37    heads: ReadStorage<'a, Heads>,
38    bodies: ReadStorage<'a, Body>,
39    masses: ReadStorage<'a, Mass>,
40    scales: ReadStorage<'a, Scale>,
41    physics_states: ReadStorage<'a, PhysicsState>,
42    melee_attacks: ReadStorage<'a, Melee>,
43    beams: ReadStorage<'a, Beam>,
44    uids: ReadStorage<'a, Uid>,
45    is_riders: ReadStorage<'a, Is<Rider>>,
46    is_volume_riders: ReadStorage<'a, Is<VolumeRider>>,
47    stats: ReadStorage<'a, Stats>,
48    skill_sets: ReadStorage<'a, SkillSet>,
49    active_abilities: ReadStorage<'a, ActiveAbilities>,
50    msm: ReadExpect<'a, MaterialStatManifest>,
51    ability_map: ReadExpect<'a, AbilityMap>,
52    combos: ReadStorage<'a, Combo>,
53    alignments: ReadStorage<'a, comp::Alignment>,
54    terrain: ReadExpect<'a, TerrainGrid>,
55    inventories: ReadStorage<'a, Inventory>,
56    stances: ReadStorage<'a, Stance>,
57    prev_phys_caches: ReadStorage<'a, PreviousPhysCache>,
58}
59
60/// ## Character Behavior System
61/// Passes `JoinData` to `CharacterState`'s `behavior` handler fn's. Receives a
62/// `StateUpdate` in return and performs updates to ECS Components from that.
63#[derive(Default)]
64pub struct Sys;
65
66impl<'a> System<'a> for Sys {
67    type SystemData = (
68        ReadData<'a>,
69        WriteStorage<'a, CharacterState>,
70        WriteStorage<'a, CharacterActivity>,
71        WriteStorage<'a, Pos>,
72        WriteStorage<'a, Vel>,
73        WriteStorage<'a, Ori>,
74        WriteStorage<'a, Density>,
75        WriteStorage<'a, Energy>,
76        WriteStorage<'a, Controller>,
77        WriteStorage<'a, Poise>,
78        Read<'a, EventBus<Outcome>>,
79        Read<'a, IdMaps>,
80    );
81
82    const NAME: &'static str = "character_behavior";
83    const ORIGIN: Origin = Origin::Common;
84    const PHASE: Phase = Phase::Create;
85
86    fn run(
87        _job: &mut Job<Self>,
88        (
89            read_data,
90            mut character_states,
91            mut character_activities,
92            mut positions,
93            mut velocities,
94            mut orientations,
95            mut densities,
96            mut energies,
97            mut controllers,
98            mut poises,
99            outcomes,
100            id_maps,
101        ): Self::SystemData,
102    ) {
103        let mut local_emitter = read_data.local_bus.emitter();
104        let mut outcomes_emitter = outcomes.emitter();
105        let mut emitters = read_data.events.get_emitters();
106
107        let mut local_events = Vec::new();
108        let mut output_events = OutputEvents::new(&mut local_events, &mut emitters);
109
110        let join = (
111            &read_data.entities,
112            &read_data.uids,
113            &mut character_states,
114            &mut character_activities,
115            &mut positions,
116            &mut velocities,
117            &mut orientations,
118            &read_data.masses,
119            &mut densities,
120            &mut energies,
121            read_data.inventories.maybe(),
122            &mut controllers,
123            read_data.healths.maybe(),
124            read_data.heads.maybe(),
125            (
126                &read_data.bodies,
127                &read_data.physics_states,
128                read_data.scales.maybe(),
129                &read_data.stats,
130                &read_data.skill_sets,
131                read_data.active_abilities.maybe(),
132                read_data.is_riders.maybe(),
133            ),
134            read_data.combos.maybe(),
135        )
136            .lend_join();
137        join.for_each(|comps| {
138            let (
139                entity,
140                uid,
141                mut char_state,
142                character_activity,
143                pos,
144                vel,
145                ori,
146                mass,
147                density,
148                energy,
149                inventory,
150                controller,
151                health,
152                heads,
153                (body, physics, scale, stat, skill_set, active_abilities, is_rider),
154                combo,
155            ) = comps;
156            // Being dead overrides all other states
157            if health.is_some_and(|h| h.is_dead) {
158                // Do nothing
159                return;
160            }
161
162            // Remove components that entity should not have if not in relevant char state
163            if !char_state.is_melee_attack() {
164                read_data.lazy_update.remove::<Melee>(entity);
165            }
166            if !char_state.is_beam_attack() {
167                read_data.lazy_update.remove::<Beam>(entity);
168            }
169
170            // Enter stunned state if poise damage is enough
171            if let Some(mut poise) = poises.get_mut(entity) {
172                let was_wielded = char_state.is_wield();
173                let poise_state = poise.poise_state();
174                let pos = pos.0;
175                if let (Some((stunned_state, stunned_duration)), impulse_strength) =
176                    poise_state.poise_effect(was_wielded)
177                {
178                    // Reset poise if there is some stunned state to apply
179                    poise.reset(*read_data.time, stunned_duration);
180                    if !comp::is_downed(health, Some(&char_state)) {
181                        *char_state = stunned_state;
182                    }
183                    outcomes_emitter.emit(Outcome::PoiseChange {
184                        pos,
185                        state: poise_state,
186                    });
187                    if let Some(impulse_strength) = impulse_strength {
188                        output_events.emit_server(KnockbackEvent {
189                            entity,
190                            impulse: impulse_strength * *poise.knockback(),
191                        });
192                    }
193                }
194            }
195
196            // Controller actions
197            let actions = std::mem::take(&mut controller.actions);
198
199            let mut join_struct = JoinStruct {
200                entity,
201                uid,
202                char_state,
203                character_activity,
204                pos,
205                vel,
206                ori,
207                scale,
208                mass,
209                density,
210                energy,
211                inventory,
212                controller,
213                health,
214                heads,
215                body,
216                physics,
217                melee_attack: read_data.melee_attacks.get(entity),
218                beam: read_data.beams.get(entity),
219                stat,
220                skill_set,
221                active_abilities,
222                combo,
223                alignment: read_data.alignments.get(entity),
224                terrain: &read_data.terrain,
225                mount_data: read_data.is_riders.get(entity),
226                volume_mount_data: read_data.is_volume_riders.get(entity),
227                stance: read_data.stances.get(entity),
228                id_maps: &id_maps,
229                alignments: &read_data.alignments,
230                prev_phys_caches: &read_data.prev_phys_caches,
231            };
232
233            for action in actions {
234                let j = JoinData::new(
235                    &join_struct,
236                    &read_data.lazy_update,
237                    &read_data.dt,
238                    &read_data.time,
239                    &read_data.msm,
240                    &read_data.ability_map,
241                );
242                let state_update = j.character.handle_event(&j, &mut output_events, action);
243                Self::publish_state_update(&mut join_struct, state_update, &mut output_events);
244            }
245
246            // Mounted occurs after control actions have been handled
247            // If mounted, character state is controlled by mount
248            if is_rider.is_some() && !join_struct.char_state.can_perform_mounted() {
249                // TODO: A better way to swap between mount inputs and rider inputs
250                *join_struct.char_state = CharacterState::Idle(idle::Data::default());
251                return;
252            }
253
254            let j = JoinData::new(
255                &join_struct,
256                &read_data.lazy_update,
257                &read_data.dt,
258                &read_data.time,
259                &read_data.msm,
260                &read_data.ability_map,
261            );
262
263            let state_update = j.character.behavior(&j, &mut output_events);
264            Self::publish_state_update(&mut join_struct, state_update, &mut output_events);
265        });
266
267        local_emitter.append_vec(local_events);
268    }
269}
270
271impl Sys {
272    fn publish_state_update(
273        join: &mut JoinStruct,
274        state_update: StateUpdate,
275        output_events: &mut OutputEvents,
276    ) {
277        // Here we check for equality with the previous value of these components before
278        // updating them so that the modification detection will not be
279        // triggered unnecessarily. This is important for minimizing updates
280        // sent to the clients (and thus keeping bandwidth usage down).
281        //
282        // TODO: if checking equality is expensive for char_state use optional field in
283        // StateUpdate
284        if *join.char_state != state_update.character {
285            *join.char_state = state_update.character
286        }
287        if *join.character_activity != state_update.character_activity {
288            *join.character_activity = state_update.character_activity
289        }
290        if *join.density != state_update.density {
291            *join.density = state_update.density
292        }
293        if *join.energy != state_update.energy {
294            *join.energy = state_update.energy;
295        };
296
297        // These components use a different type of change detection.
298        *join.pos = state_update.pos;
299        *join.vel = state_update.vel;
300        *join.ori = state_update.ori;
301
302        for (input, attr) in state_update.queued_inputs {
303            join.controller.queued_inputs.insert(input, attr);
304        }
305        for input in state_update.removed_inputs {
306            join.controller.queued_inputs.remove(&input);
307        }
308        if state_update.swap_equipped_weapons {
309            output_events.emit_server(event::InventoryManipEvent(
310                join.entity,
311                InventoryManip::SwapEquippedWeapons,
312            ));
313        }
314    }
315}