veloren_common/states/
utils.rs

1use crate::{
2    astar::Astar,
3    comp::{
4        Alignment, Body, CharacterState, Density, InputAttr, InputKind, InventoryAction, Melee,
5        Ori, Pos, StateUpdate,
6        ability::{AbilityInitEvent, AbilityMeta, Capability, SpecifiedAbility, Stance},
7        arthropod, biped_large, biped_small, bird_medium,
8        buff::{Buff, BuffCategory, BuffChange, BuffData, BuffSource, DestInfo},
9        character_state::OutputEvents,
10        controller::InventoryManip,
11        crustacean, golem,
12        inventory::slot::{ArmorSlot, EquipSlot, Slot},
13        item::{
14            Hands, ItemKind, ToolKind,
15            armor::Friction,
16            tool::{self, AbilityContext},
17        },
18        object, quadruped_low, quadruped_medium, quadruped_small, ship,
19        skills::{SKILL_MODIFIERS, Skill, SwimSkill},
20        theropod,
21    },
22    consts::{FRIC_GROUND, GRAVITY, MAX_MOUNT_RANGE, MAX_PICKUP_RANGE},
23    event::{BuffEvent, ChangeStanceEvent, ComboChangeEvent, InventoryManipEvent, LocalEvent},
24    mounting::Volume,
25    outcome::Outcome,
26    states::{behavior::JoinData, utils::CharacterState::Idle, *},
27    terrain::{Block, TerrainGrid, UnlockKind},
28    util::Dir,
29    vol::ReadVol,
30};
31use core::hash::BuildHasherDefault;
32use fxhash::FxHasher64;
33use serde::{Deserialize, Serialize};
34use std::{
35    f32::consts::PI,
36    ops::{Add, Div, Mul},
37    time::Duration,
38};
39use strum::Display;
40use tracing::warn;
41use vek::*;
42
43pub const MOVEMENT_THRESHOLD_VEL: f32 = 3.0;
44
45impl Body {
46    pub fn base_accel(&self) -> f32 {
47        match self {
48            // Note: Entities have been slowed down relative to humanoid speeds, but it may be worth
49            // reverting/increasing speed once we've established slower AI.
50            Body::Humanoid(_) => 100.0,
51            Body::QuadrupedSmall(body) => match body.species {
52                quadruped_small::Species::Turtle => 30.0,
53                quadruped_small::Species::Axolotl => 70.0,
54                quadruped_small::Species::Pig => 70.0,
55                quadruped_small::Species::Sheep => 70.0,
56                quadruped_small::Species::Truffler => 70.0,
57                quadruped_small::Species::Fungome => 70.0,
58                quadruped_small::Species::Goat => 80.0,
59                quadruped_small::Species::Raccoon => 100.0,
60                quadruped_small::Species::Frog => 150.0,
61                quadruped_small::Species::Porcupine => 100.0,
62                quadruped_small::Species::Beaver => 100.0,
63                quadruped_small::Species::Rabbit => 110.0,
64                quadruped_small::Species::Cat => 150.0,
65                quadruped_small::Species::Quokka => 100.0,
66                quadruped_small::Species::MossySnail => 20.0,
67                _ => 125.0,
68            },
69            Body::QuadrupedMedium(quadruped_medium) => match quadruped_medium.species {
70                quadruped_medium::Species::Grolgar => 100.0,
71                quadruped_medium::Species::Saber => 110.0,
72                quadruped_medium::Species::Tiger => 110.0,
73                quadruped_medium::Species::Tuskram => 85.0,
74                quadruped_medium::Species::Lion => 105.0,
75                quadruped_medium::Species::Tarasque => 100.0,
76                quadruped_medium::Species::Wolf => 130.0,
77                quadruped_medium::Species::Frostfang => 115.0,
78                quadruped_medium::Species::Mouflon => 75.0,
79                quadruped_medium::Species::Catoblepas => 60.0,
80                quadruped_medium::Species::Bonerattler => 115.0,
81                quadruped_medium::Species::Deer => 120.0,
82                quadruped_medium::Species::Hirdrasil => 110.0,
83                quadruped_medium::Species::Roshwalr => 70.0,
84                quadruped_medium::Species::Donkey => 90.0,
85                quadruped_medium::Species::Camel => 75.0,
86                quadruped_medium::Species::Zebra => 150.0,
87                quadruped_medium::Species::Antelope => 155.0,
88                quadruped_medium::Species::Kelpie => 140.0,
89                quadruped_medium::Species::Horse => 140.0,
90                quadruped_medium::Species::Barghest => 80.0,
91                quadruped_medium::Species::Cattle => 80.0,
92                quadruped_medium::Species::Darkhound => 115.0,
93                quadruped_medium::Species::Highland => 80.0,
94                quadruped_medium::Species::Yak => 80.0,
95                quadruped_medium::Species::Panda => 90.0,
96                quadruped_medium::Species::Bear => 90.0,
97                quadruped_medium::Species::Dreadhorn => 95.0,
98                quadruped_medium::Species::Moose => 105.0,
99                quadruped_medium::Species::Snowleopard => 115.0,
100                quadruped_medium::Species::Mammoth => 75.0,
101                quadruped_medium::Species::Ngoubou => 95.0,
102                quadruped_medium::Species::Llama => 100.0,
103                quadruped_medium::Species::Alpaca => 100.0,
104                quadruped_medium::Species::Akhlut => 90.0,
105                quadruped_medium::Species::Bristleback => 105.0,
106                quadruped_medium::Species::ClaySteed => 85.0,
107            },
108            Body::BipedLarge(body) => match body.species {
109                biped_large::Species::Slysaurok => 100.0,
110                biped_large::Species::Occultsaurok => 100.0,
111                biped_large::Species::Mightysaurok => 100.0,
112                biped_large::Species::Mindflayer => 90.0,
113                biped_large::Species::Minotaur => 60.0,
114                biped_large::Species::Huskbrute => 130.0,
115                biped_large::Species::Cultistwarlord => 110.0,
116                biped_large::Species::Cultistwarlock => 90.0,
117                biped_large::Species::Gigasfrost => 45.0,
118                biped_large::Species::Gigasfire => 50.0,
119                biped_large::Species::Forgemaster => 100.0,
120                _ => 80.0,
121            },
122            Body::BirdMedium(_) => 80.0,
123            Body::FishMedium(_) => 80.0,
124            Body::Dragon(_) => 250.0,
125            Body::BirdLarge(_) => 110.0,
126            Body::FishSmall(_) => 60.0,
127            Body::BipedSmall(biped_small) => match biped_small.species {
128                biped_small::Species::Haniwa => 65.0,
129                biped_small::Species::Boreal => 100.0,
130                biped_small::Species::Gnarling => 70.0,
131                _ => 80.0,
132            },
133            Body::Object(_) => 0.0,
134            Body::Item(_) => 0.0,
135            Body::Golem(body) => match body.species {
136                golem::Species::ClayGolem => 120.0,
137                golem::Species::IronGolem => 100.0,
138                _ => 60.0,
139            },
140            Body::Theropod(theropod) => match theropod.species {
141                theropod::Species::Archaeos
142                | theropod::Species::Odonto
143                | theropod::Species::Ntouka => 110.0,
144                theropod::Species::Dodarock => 75.0,
145                theropod::Species::Yale => 115.0,
146                _ => 125.0,
147            },
148            Body::QuadrupedLow(quadruped_low) => match quadruped_low.species {
149                quadruped_low::Species::Crocodile => 60.0,
150                quadruped_low::Species::SeaCrocodile => 60.0,
151                quadruped_low::Species::Alligator => 65.0,
152                quadruped_low::Species::Salamander => 85.0,
153                quadruped_low::Species::Elbst => 85.0,
154                quadruped_low::Species::Monitor => 130.0,
155                quadruped_low::Species::Asp => 100.0,
156                quadruped_low::Species::Tortoise => 60.0,
157                quadruped_low::Species::Rocksnapper => 70.0,
158                quadruped_low::Species::Rootsnapper => 70.0,
159                quadruped_low::Species::Reefsnapper => 70.0,
160                quadruped_low::Species::Pangolin => 90.0,
161                quadruped_low::Species::Maneater => 80.0,
162                quadruped_low::Species::Sandshark => 125.0,
163                quadruped_low::Species::Hakulaq => 125.0,
164                quadruped_low::Species::Dagon => 140.0,
165                quadruped_low::Species::Lavadrake => 100.0,
166                quadruped_low::Species::Icedrake => 100.0,
167                quadruped_low::Species::Basilisk => 85.0,
168                quadruped_low::Species::Deadwood => 110.0,
169                quadruped_low::Species::Mossdrake => 100.0,
170                quadruped_low::Species::Driggle => 120.0,
171                quadruped_low::Species::Snaretongue => 120.0,
172                quadruped_low::Species::Hydra => 100.0,
173            },
174            Body::Ship(ship::Body::Carriage) => 40.0,
175            Body::Ship(ship::Body::Train) => 9.0,
176            Body::Ship(_) => 0.0,
177            Body::Arthropod(arthropod) => match arthropod.species {
178                arthropod::Species::Tarantula => 85.0,
179                arthropod::Species::Blackwidow => 95.0,
180                arthropod::Species::Antlion => 115.0,
181                arthropod::Species::Hornbeetle => 80.0,
182                arthropod::Species::Leafbeetle => 65.0,
183                arthropod::Species::Stagbeetle => 80.0,
184                arthropod::Species::Weevil => 70.0,
185                arthropod::Species::Cavespider => 90.0,
186                arthropod::Species::Moltencrawler => 70.0,
187                arthropod::Species::Mosscrawler => 70.0,
188                arthropod::Species::Sandcrawler => 70.0,
189                arthropod::Species::Dagonite => 70.0,
190                arthropod::Species::Emberfly => 75.0,
191            },
192            Body::Crustacean(body) => match body.species {
193                crustacean::Species::Crab | crustacean::Species::SoldierCrab => 80.0,
194                crustacean::Species::Karkatha => 120.0,
195            },
196            Body::Plugin(body) => body.base_accel(),
197        }
198    }
199
200    pub fn air_accel(&self) -> f32 { self.base_accel() * 0.025 }
201
202    /// Attempt to determine the maximum speed of the character
203    /// when moving on the ground
204    pub fn max_speed_approx(&self) -> f32 {
205        // Inverse kinematics: at what velocity will acceleration
206        // be cancelled out by friction drag?
207        // Note: we assume no air, since it's such a small factor.
208        // Derived via...
209        // v = (v + dv / 30) * (1 - drag).powi(2) (accel cancels drag)
210        // => 1 = (1 + (dv / 30) / v) * (1 - drag).powi(2)
211        // => 1 / (1 - drag).powi(2) = 1 + (dv / 30) / v
212        // => 1 / (1 - drag).powi(2) - 1 = (dv / 30) / v
213        // => 1 / (1 / (1 - drag).powi(2) - 1) = v / (dv / 30)
214        // => (dv / 30) / (1 / (1 - drag).powi(2) - 1) = v
215        let v = match self {
216            Body::Ship(ship) => ship.get_speed(),
217            _ => (-self.base_accel() / 30.0) / ((1.0 - FRIC_GROUND).powi(2) - 1.0),
218        };
219        debug_assert!(v >= 0.0, "Speed must be positive!");
220        v
221    }
222
223    /// The turn rate in 180°/s (or (rotations per second)/2)
224    pub fn base_ori_rate(&self) -> f32 {
225        match self {
226            Body::Humanoid(_) => 3.5,
227            Body::QuadrupedSmall(_) => 3.0,
228            Body::QuadrupedMedium(quadruped_medium) => match quadruped_medium.species {
229                quadruped_medium::Species::Mammoth => 1.0,
230                _ => 2.8,
231            },
232            Body::BirdMedium(_) => 6.0,
233            Body::FishMedium(_) => 6.0,
234            Body::Dragon(_) => 1.0,
235            Body::BirdLarge(_) => 7.0,
236            Body::FishSmall(_) => 7.0,
237            Body::BipedLarge(biped_large) => match biped_large.species {
238                biped_large::Species::Harvester => 2.0,
239                _ => 2.7,
240            },
241            Body::BipedSmall(_) => 3.5,
242            Body::Object(_) => 2.0,
243            Body::Item(_) => 2.0,
244            Body::Golem(golem) => match golem.species {
245                golem::Species::WoodGolem => 1.2,
246                _ => 2.0,
247            },
248            Body::Theropod(theropod) => match theropod.species {
249                theropod::Species::Archaeos => 2.3,
250                theropod::Species::Odonto => 2.3,
251                theropod::Species::Ntouka => 2.3,
252                theropod::Species::Dodarock => 2.0,
253                _ => 2.5,
254            },
255            Body::QuadrupedLow(quadruped_low) => match quadruped_low.species {
256                quadruped_low::Species::Asp => 2.2,
257                quadruped_low::Species::Tortoise => 1.5,
258                quadruped_low::Species::Rocksnapper => 1.8,
259                quadruped_low::Species::Rootsnapper => 1.8,
260                quadruped_low::Species::Lavadrake => 1.7,
261                quadruped_low::Species::Icedrake => 1.7,
262                quadruped_low::Species::Mossdrake => 1.7,
263                _ => 2.0,
264            },
265            Body::Ship(ship::Body::Carriage) => 0.04,
266            Body::Ship(ship::Body::Train) => 0.0,
267            Body::Ship(ship) if ship.has_water_thrust() => 5.0 / self.dimensions().y,
268            Body::Ship(_) => 6.0 / self.dimensions().y,
269            Body::Arthropod(_) => 3.5,
270            Body::Crustacean(_) => 3.5,
271            Body::Plugin(body) => body.base_ori_rate(),
272        }
273    }
274
275    /// Returns thrust force if the body type can swim, otherwise None
276    pub fn swim_thrust(&self) -> Option<f32> {
277        // Swim thrust is proportional to the frontal area of the creature, since we
278        // assume that strength roughly scales according to square laws. Also,
279        // it happens to make balancing against drag much simpler.
280        let front_profile = self.dimensions().x * self.dimensions().z;
281        Some(
282            match self {
283                Body::Object(_) => return None,
284                Body::Item(_) => return None,
285                Body::Ship(ship::Body::Submarine) => 1000.0 * self.mass().0,
286                Body::Ship(ship) if ship.has_water_thrust() => 500.0 * self.mass().0,
287                Body::Ship(_) => return None,
288                Body::BipedLarge(_) => 120.0 * self.mass().0,
289                Body::Golem(_) => 100.0 * self.mass().0,
290                Body::BipedSmall(_) => 1000.0 * self.mass().0,
291                Body::BirdMedium(_) => 400.0 * self.mass().0,
292                Body::BirdLarge(_) => 400.0 * self.mass().0,
293                Body::FishMedium(_) => 200.0 * self.mass().0,
294                Body::FishSmall(_) => 300.0 * self.mass().0,
295                Body::Dragon(_) => 50.0 * self.mass().0,
296                // Humanoids are a bit different: we try to give them thrusts that result in similar
297                // speeds for gameplay reasons
298                Body::Humanoid(_) => 4_000_000.0 / self.mass().0,
299                Body::Theropod(body) => match body.species {
300                    theropod::Species::Sandraptor
301                    | theropod::Species::Snowraptor
302                    | theropod::Species::Sunlizard
303                    | theropod::Species::Woodraptor
304                    | theropod::Species::Dodarock
305                    | theropod::Species::Axebeak
306                    | theropod::Species::Yale => 500.0 * self.mass().0,
307                    _ => 150.0 * self.mass().0,
308                },
309                Body::QuadrupedLow(_) => 1200.0 * self.mass().0,
310                Body::QuadrupedMedium(body) => match body.species {
311                    quadruped_medium::Species::Mammoth => 150.0 * self.mass().0,
312                    quadruped_medium::Species::Kelpie => 3500.0 * self.mass().0,
313                    _ => 1000.0 * self.mass().0,
314                },
315                Body::QuadrupedSmall(_) => 1500.0 * self.mass().0,
316                Body::Arthropod(_) => 500.0 * self.mass().0,
317                Body::Crustacean(_) => 400.0 * self.mass().0,
318                Body::Plugin(body) => body.swim_thrust()?,
319            } * front_profile,
320        )
321    }
322
323    /// Returns thrust force if the body type can fly, otherwise None
324    pub fn fly_thrust(&self) -> Option<f32> {
325        match self {
326            Body::BirdMedium(body) => match body.species {
327                bird_medium::Species::Bat | bird_medium::Species::BloodmoonBat => {
328                    Some(GRAVITY * self.mass().0 * 0.5)
329                },
330                _ => Some(GRAVITY * self.mass().0 * 2.0),
331            },
332            Body::BirdLarge(_) => Some(GRAVITY * self.mass().0 * 0.5),
333            Body::Dragon(_) => Some(200_000.0),
334            Body::Ship(ship) if ship.can_fly() => Some(390_000.0),
335            Body::Object(object::Body::Crux) => Some(1_000.0),
336            _ => None,
337        }
338    }
339
340    /// Returns whether the body uses vectored propulsion
341    pub fn vectored_propulsion(&self) -> bool {
342        match self {
343            Body::Ship(ship) => ship.vectored_propulsion(),
344            _ => false,
345        }
346    }
347
348    /// Returns jump impulse if the body type can jump, otherwise None
349    pub fn jump_impulse(&self) -> Option<f32> {
350        match self {
351            Body::Object(_) | Body::Ship(_) | Body::Item(_) => None,
352            Body::BipedLarge(_) | Body::Dragon(_) => Some(0.6 * self.mass().0),
353            Body::Golem(_) | Body::QuadrupedLow(_) => Some(0.4 * self.mass().0),
354            Body::QuadrupedMedium(_) => Some(0.4 * self.mass().0),
355            Body::Theropod(body) => match body.species {
356                theropod::Species::Snowraptor
357                | theropod::Species::Sandraptor
358                | theropod::Species::Woodraptor => Some(0.4 * self.mass().0),
359                _ => None,
360            },
361            Body::Arthropod(_) => Some(1.0 * self.mass().0),
362            _ => Some(0.4 * self.mass().0),
363        }
364        .map(|f| f * GRAVITY)
365    }
366
367    pub fn can_climb(&self) -> bool { matches!(self, Body::Humanoid(_)) }
368
369    /// Returns how well a body can move backwards while strafing (0.0 = not at
370    /// all, 1.0 = same as forward)
371    pub fn reverse_move_factor(&self) -> f32 { 0.45 }
372
373    /// Returns the position where a projectile should be fired relative to this
374    /// body
375    pub fn projectile_offsets(&self, ori: Vec3<f32>, scale: f32) -> Vec3<f32> {
376        let body_offsets_z = match self {
377            Body::Golem(_) => self.height() * 0.4,
378            _ => self.eye_height(scale),
379        };
380
381        let dim = self.dimensions();
382        // The width (shoulder to shoulder) and length (nose to tail)
383        let (width, length) = (dim.x, dim.y);
384        let body_radius = if length > width {
385            // Dachshund-like
386            self.max_radius()
387        } else {
388            // Cyclops-like
389            self.min_radius()
390        };
391
392        Vec3::new(
393            body_radius * ori.x * 1.1,
394            body_radius * ori.y * 1.1,
395            body_offsets_z,
396        )
397    }
398}
399
400/// set footwear in idle data and potential state change to Skate
401pub fn handle_skating(data: &JoinData, update: &mut StateUpdate) {
402    if let &Idle(idle::Data {
403        ref is_sneaking,
404        ref time_entered,
405        mut footwear,
406    }) = data.character
407    {
408        if footwear.is_none() {
409            footwear = data.inventory.and_then(|inv| {
410                inv.equipped(EquipSlot::Armor(ArmorSlot::Feet))
411                    .map(|armor| match armor.kind().as_ref() {
412                        ItemKind::Armor(a) => {
413                            a.stats(data.msm, armor.stats_durability_multiplier())
414                                .ground_contact
415                        },
416                        _ => Friction::Normal,
417                    })
418            });
419            update.character = Idle(idle::Data {
420                is_sneaking: *is_sneaking,
421                time_entered: *time_entered,
422                footwear,
423            });
424        }
425        if data.physics.skating_active {
426            update.character =
427                CharacterState::Skate(skate::Data::new(data, footwear.unwrap_or(Friction::Normal)));
428        }
429    }
430}
431
432/// Handles updating `Components` to move player based on state of `JoinData`
433pub fn handle_move(data: &JoinData<'_>, update: &mut StateUpdate, efficiency: f32) {
434    if data.volume_mount_data.is_some() {
435        return;
436    }
437    let submersion = data
438        .physics
439        .in_liquid()
440        .map(|depth| depth / data.body.height());
441
442    if input_is_pressed(data, InputKind::Fly)
443        && submersion.is_none_or(|sub| sub < 1.0)
444        && (data.physics.on_ground.is_none() || data.body.jump_impulse().is_none())
445        && data.body.fly_thrust().is_some()
446    {
447        fly_move(data, update, efficiency);
448    } else if let Some(submersion) = (data.physics.in_liquid().is_some()
449        && data.body.swim_thrust().is_some())
450    .then_some(submersion)
451    .flatten()
452    {
453        swim_move(data, update, efficiency, submersion);
454    } else {
455        basic_move(data, update, efficiency);
456    }
457}
458
459/// Updates components to move player as if theyre on ground or in air
460fn basic_move(data: &JoinData<'_>, update: &mut StateUpdate, efficiency: f32) {
461    let efficiency = efficiency * data.stats.move_speed_modifier * data.stats.friction_modifier;
462
463    let accel = if let Some(block) = data.physics.on_ground {
464        // FRIC_GROUND temporarily used to normalize things around expected values
465        data.body.base_accel()
466            * data.scale.map_or(1.0, |s| s.0.sqrt())
467            * block.get_traction()
468            * block.get_friction()
469            / FRIC_GROUND
470    } else {
471        data.body.air_accel()
472    } * efficiency;
473
474    // Should ability to backpedal be separate from ability to strafe?
475    update.vel.0 += Vec2::broadcast(data.dt.0)
476        * accel
477        * if data.body.can_strafe() {
478            data.inputs.move_dir
479                * if is_strafing(data, update) {
480                    Lerp::lerp(
481                        Vec2::from(update.ori)
482                            .try_normalized()
483                            .unwrap_or_else(Vec2::zero)
484                            .dot(
485                                data.inputs
486                                    .move_dir
487                                    .try_normalized()
488                                    .unwrap_or_else(Vec2::zero),
489                            )
490                            .add(1.0)
491                            .div(2.0)
492                            .max(0.0),
493                        1.0,
494                        data.body.reverse_move_factor(),
495                    )
496                } else {
497                    1.0
498                }
499        } else {
500            let fw = Vec2::from(update.ori);
501            fw * data.inputs.move_dir.dot(fw).max(0.0)
502        };
503}
504
505/// Handles forced movement
506pub fn handle_forced_movement(
507    data: &JoinData<'_>,
508    update: &mut StateUpdate,
509    movement: ForcedMovement,
510) {
511    match movement {
512        ForcedMovement::Forward(strength) => {
513            let strength = strength * data.stats.move_speed_modifier * data.stats.friction_modifier;
514            if let Some(accel) = data.physics.on_ground.map(|block| {
515                // FRIC_GROUND temporarily used to normalize things around expected values
516                data.body.base_accel() * block.get_traction() * block.get_friction() / FRIC_GROUND
517            }) {
518                update.vel.0 += Vec2::broadcast(data.dt.0)
519                    * accel
520                    * data.scale.map_or(1.0, |s| s.0.sqrt())
521                    * Vec2::from(*data.ori)
522                    * strength;
523            }
524        },
525        ForcedMovement::Reverse(strength) => {
526            let strength = strength * data.stats.move_speed_modifier * data.stats.friction_modifier;
527            if let Some(accel) = data.physics.on_ground.map(|block| {
528                // FRIC_GROUND temporarily used to normalize things around expected values
529                data.body.base_accel() * block.get_traction() * block.get_friction() / FRIC_GROUND
530            }) {
531                update.vel.0 += Vec2::broadcast(data.dt.0)
532                    * accel
533                    * data.scale.map_or(1.0, |s| s.0.sqrt())
534                    * -Vec2::from(*data.ori)
535                    * strength;
536            }
537        },
538        ForcedMovement::Sideways(strength) => {
539            let strength = strength * data.stats.move_speed_modifier * data.stats.friction_modifier;
540            if let Some(accel) = data.physics.on_ground.map(|block| {
541                // FRIC_GROUND temporarily used to normalize things around expected values
542                data.body.base_accel() * block.get_traction() * block.get_friction() / FRIC_GROUND
543            }) {
544                let direction = {
545                    // Left if positive, else right
546                    let side = Vec2::from(*data.ori)
547                        .rotated_z(PI / 2.)
548                        .dot(data.inputs.move_dir)
549                        .signum();
550                    if side > 0.0 {
551                        Vec2::from(*data.ori).rotated_z(PI / 2.)
552                    } else {
553                        -Vec2::from(*data.ori).rotated_z(PI / 2.)
554                    }
555                };
556
557                update.vel.0 += Vec2::broadcast(data.dt.0)
558                    * accel
559                    * data.scale.map_or(1.0, |s| s.0.sqrt())
560                    * direction
561                    * strength;
562            }
563        },
564        ForcedMovement::DirectedReverse(strength) => {
565            let strength = strength * data.stats.move_speed_modifier * data.stats.friction_modifier;
566            if let Some(accel) = data.physics.on_ground.map(|block| {
567                // FRIC_GROUND temporarily used to normalize things around expected values
568                data.body.base_accel() * block.get_traction() * block.get_friction() / FRIC_GROUND
569            }) {
570                let direction = if Vec2::from(*data.ori).dot(data.inputs.move_dir).signum() > 0.0 {
571                    data.inputs.move_dir.reflected(Vec2::from(*data.ori))
572                } else {
573                    data.inputs.move_dir
574                }
575                .try_normalized()
576                .unwrap_or_else(|| -Vec2::from(*data.ori));
577                update.vel.0 += direction * strength * accel * data.dt.0;
578            }
579        },
580        ForcedMovement::AntiDirectedForward(strength) => {
581            let strength = strength * data.stats.move_speed_modifier * data.stats.friction_modifier;
582            if let Some(accel) = data.physics.on_ground.map(|block| {
583                // FRIC_GROUND temporarily used to normalize things around expected values
584                data.body.base_accel() * block.get_traction() * block.get_friction() / FRIC_GROUND
585            }) {
586                let direction = if Vec2::from(*data.ori).dot(data.inputs.move_dir).signum() < 0.0 {
587                    data.inputs.move_dir.reflected(Vec2::from(*data.ori))
588                } else {
589                    data.inputs.move_dir
590                }
591                .try_normalized()
592                .unwrap_or_else(|| Vec2::from(*data.ori));
593                let direction = direction.reflected(Vec2::from(*data.ori).rotated_z(PI / 2.));
594                update.vel.0 += direction * strength * accel * data.dt.0;
595            }
596        },
597        ForcedMovement::Leap {
598            vertical,
599            forward,
600            progress,
601            direction,
602        } => {
603            let dir = direction.get_2d_dir(data);
604            // Apply jumping force
605            update.vel.0 = Vec3::new(
606                dir.x,
607                dir.y,
608                vertical,
609            )
610                * data.scale.map_or(1.0, |s| s.0.sqrt())
611                // Multiply decreasing amount linearly over time (with average of 1)
612                * 2.0 * progress
613                // Apply direction
614                + Vec3::from(dir)
615                // Multiply by forward leap strength
616                * forward
617                // Control forward movement based on look direction.
618                // This allows players to stop moving forward when they
619                // look downward at target
620                * (1.0 - data.inputs.look_dir.z.abs());
621        },
622        ForcedMovement::Hover { move_input } => {
623            update.vel.0 = Vec3::new(data.vel.0.x, data.vel.0.y, 0.0)
624                + move_input
625                    * data.scale.map_or(1.0, |s| s.0.sqrt())
626                    * data.inputs.move_dir.try_normalized().unwrap_or_default();
627        },
628    }
629}
630
631pub fn handle_orientation(
632    data: &JoinData<'_>,
633    update: &mut StateUpdate,
634    efficiency: f32,
635    dir_override: Option<Dir>,
636) {
637    /// first check for horizontal
638    fn to_horizontal_fast(ori: &crate::comp::Ori) -> crate::comp::Ori {
639        if ori.to_quat().into_vec4().xy().is_approx_zero() {
640            *ori
641        } else {
642            ori.to_horizontal()
643        }
644    }
645    /// compute an upper limit for the difference of two orientations
646    fn ori_absdiff(a: &crate::comp::Ori, b: &crate::comp::Ori) -> f32 {
647        (a.to_quat().into_vec4() - b.to_quat().into_vec4()).reduce(|a, b| a.abs() + b.abs())
648    }
649
650    let (tilt_ori, efficiency) = if let Body::Ship(ship) = data.body
651        && ship.has_wheels()
652    {
653        let height_at = |rpos| {
654            data.terrain
655                .ray(
656                    data.pos.0 + rpos + Vec3::unit_z() * 4.0,
657                    data.pos.0 + rpos - Vec3::unit_z() * 4.0,
658                )
659                .until(Block::is_solid)
660                .cast()
661                .0
662        };
663
664        // Do some cheap raycasting with the ground to determine the appropriate
665        // orientation for the vehicle
666        let x_diff = (height_at(data.ori.to_horizontal().right().to_vec() * 3.0)
667            - height_at(data.ori.to_horizontal().right().to_vec() * -3.0))
668            / 10.0;
669        let y_diff = (height_at(data.ori.to_horizontal().look_dir().to_vec() * -4.5)
670            - height_at(data.ori.to_horizontal().look_dir().to_vec() * 4.5))
671            / 10.0;
672
673        (
674            Quaternion::rotation_y(x_diff.atan()) * Quaternion::rotation_x(y_diff.atan()),
675            (data.vel.0 - data.physics.ground_vel)
676                .xy()
677                .magnitude()
678                .max(3.0)
679                * efficiency,
680        )
681    } else {
682        (Quaternion::identity(), efficiency)
683    };
684
685    // Direction is set to the override if one is provided, else if entity is
686    // strafing or attacking the horiontal component of the look direction is used,
687    // else the current horizontal movement direction is used
688    let target_ori = if let Some(dir_override) = dir_override {
689        dir_override.into()
690    } else if is_strafing(data, update) || update.character.should_follow_look() {
691        data.inputs
692            .look_dir
693            .to_horizontal()
694            .unwrap_or_default()
695            .into()
696    } else {
697        Dir::from_unnormalized(data.inputs.move_dir.into())
698            .map_or_else(|| to_horizontal_fast(data.ori), |dir| dir.into())
699    }
700    .rotated(tilt_ori);
701    // unit is multiples of 180°
702    let half_turns_per_tick = data.body.base_ori_rate() / data.scale.map_or(1.0, |s| s.0.sqrt())
703        * efficiency
704        * if data.physics.on_ground.is_some() {
705            1.0
706        } else if data.physics.in_liquid().is_some() {
707            0.4
708        } else {
709            0.2
710        }
711        * data.dt.0;
712    // very rough guess
713    let ticks_from_target_guess = ori_absdiff(&update.ori, &target_ori) / half_turns_per_tick;
714    let instantaneous = ticks_from_target_guess < 1.0;
715    update.ori = if data.volume_mount_data.is_some() {
716        update.ori
717    } else if instantaneous {
718        target_ori
719    } else {
720        let target_fraction = {
721            // Angle factor used to keep turning rate approximately constant by
722            // counteracting slerp turning more with a larger angle
723            let angle_factor = 2.0 / (1.0 - update.ori.dot(target_ori)).sqrt();
724
725            half_turns_per_tick * angle_factor
726        };
727        update
728            .ori
729            .slerped_towards(target_ori, target_fraction.min(1.0))
730    };
731
732    // Look at things
733    update.character_activity.look_dir = Some(data.controller.inputs.look_dir);
734}
735
736/// Updates components to move player as if theyre swimming
737fn swim_move(
738    data: &JoinData<'_>,
739    update: &mut StateUpdate,
740    efficiency: f32,
741    submersion: f32,
742) -> bool {
743    let efficiency = efficiency * data.stats.swim_speed_modifier * data.stats.friction_modifier;
744    if let Some(force) = data.body.swim_thrust() {
745        let force = efficiency * force * data.scale.map_or(1.0, |s| s.0);
746        let mut water_accel = force / data.mass.0;
747
748        if let Ok(level) = data.skill_set.skill_level(Skill::Swim(SwimSkill::Speed)) {
749            let modifiers = SKILL_MODIFIERS.general_tree.swim;
750            water_accel *= modifiers.speed.powi(level.into());
751        }
752
753        let dir = if data.body.can_strafe() {
754            data.inputs.move_dir
755        } else {
756            let fw = Vec2::from(update.ori);
757            fw * data.inputs.move_dir.dot(fw).max(0.0)
758        };
759
760        // Automatically tread water to stay afloat
761        let move_z = if submersion < 1.0
762            && data.inputs.move_z.abs() < f32::EPSILON
763            && data.physics.on_ground.is_none()
764        {
765            submersion.max(0.0) * 0.1
766        } else {
767            data.inputs.move_z
768        };
769
770        // Assume that feet/flippers get less efficient as we become less submerged
771        let move_z = move_z.min((submersion * 1.5 - 0.5).clamp(0.0, 1.0).powi(2));
772
773        update.vel.0 += Vec3::new(dir.x, dir.y, move_z)
774                // TODO: Should probably be normalised, but creates odd discrepancies when treading water
775                // .try_normalized()
776                // .unwrap_or_default()
777            * water_accel
778            // Gives a good balance between submerged and surface speed
779            * submersion.clamp(0.0, 1.0).sqrt()
780            // Good approximate compensation for dt-dependent effects
781            * data.dt.0 * 0.04;
782
783        true
784    } else {
785        false
786    }
787}
788
789/// Updates components to move entity as if it's flying
790pub fn fly_move(data: &JoinData<'_>, update: &mut StateUpdate, efficiency: f32) -> bool {
791    let efficiency = efficiency * data.stats.move_speed_modifier * data.stats.friction_modifier;
792
793    let glider = match data.character {
794        CharacterState::Glide(data) => Some(data),
795        _ => None,
796    };
797    if let Some(force) = data
798        .body
799        .fly_thrust()
800        .or_else(|| glider.is_some().then_some(0.0))
801    {
802        let thrust = efficiency * force;
803        let accel = thrust / data.mass.0;
804
805        match data.body {
806            Body::Ship(ship::Body::DefaultAirship) => {
807                // orient the airship according to the controller look_dir
808                // Make the airship rotation more efficient (x2) so that it
809                // can orient itself more quickly.
810                handle_orientation(
811                    data,
812                    update,
813                    efficiency * 2.0,
814                    Some(data.controller.inputs.look_dir),
815                );
816            },
817            _ => {
818                handle_orientation(data, update, efficiency, None);
819            },
820        }
821
822        let mut update_fw_vel = true;
823        // Elevation control
824        match data.body {
825            // flappy flappy
826            Body::Dragon(_) | Body::BirdLarge(_) | Body::BirdMedium(_) => {
827                let anti_grav = GRAVITY * (1.0 + data.inputs.move_z.min(0.0));
828                update.vel.0.z += data.dt.0 * (anti_grav + accel * data.inputs.move_z.max(0.0));
829            },
830            // led zeppelin
831            Body::Ship(ship::Body::DefaultAirship) => {
832                update_fw_vel = false;
833                // airships or zeppelins are controlled by their engines and should have
834                // neutral buoyancy. Don't change their density.
835                // Assume that the airship is always level and that the engines are gimbaled
836                // so that they can provide thrust in any direction.
837                // The vector of thrust is the desired movement direction scaled by the
838                // acceleration.
839                let thrust_dir = data.inputs.move_dir.with_z(data.inputs.move_z);
840                update.vel.0 += thrust_dir * data.dt.0 * accel;
841            },
842            // floaty floaty
843            Body::Ship(ship) if ship.can_fly() => {
844                // Balloons gain altitude by modifying their density, e.g. by heating the air
845                // inside. Ships float by adjusting their buoyancy, e.g. by
846                // pumping water in or out. Simulate a ship or balloon by
847                // adjusting its density.
848                let regulate_density = |min: f32, max: f32, def: f32, rate: f32| -> Density {
849                    // Reset to default on no input
850                    let change = if data.inputs.move_z.abs() > f32::EPSILON {
851                        -data.inputs.move_z
852                    } else {
853                        (def - data.density.0).clamp(-1.0, 1.0)
854                    };
855                    Density((update.density.0 + data.dt.0 * rate * change).clamp(min, max))
856                };
857                let def_density = ship.density().0;
858                if data.physics.in_liquid().is_some() {
859                    let hull_density = ship.hull_density().0;
860                    update.density.0 =
861                        regulate_density(def_density * 0.6, hull_density, hull_density, 25.0).0;
862                } else {
863                    update.density.0 =
864                        regulate_density(def_density * 0.5, def_density * 1.5, def_density, 0.5).0;
865                };
866            },
867            // oopsie woopsie
868            // TODO: refactor to make this state impossible
869            _ => {},
870        };
871
872        if update_fw_vel {
873            update.vel.0 += Vec2::broadcast(data.dt.0)
874                * accel
875                * if data.body.can_strafe() {
876                    data.inputs.move_dir
877                } else {
878                    let fw = Vec2::from(update.ori);
879                    fw * data.inputs.move_dir.dot(fw).max(0.0)
880                };
881        }
882        true
883    } else {
884        false
885    }
886}
887
888/// Checks if an input related to an attack is held. If one is, moves entity
889/// into wielding state
890pub fn handle_wield(data: &JoinData<'_>, update: &mut StateUpdate) {
891    if data.controller.queued_inputs.keys().any(|i| i.is_ability()) {
892        attempt_wield(data, update);
893    }
894}
895
896/// If a tool is equipped, goes into Equipping state, otherwise goes to Idle
897pub fn attempt_wield(data: &JoinData<'_>, update: &mut StateUpdate) {
898    // Closure to get equip time provided an equip slot if a tool is equipped in
899    // equip slot
900    let equip_time = |equip_slot| {
901        data.inventory
902            .and_then(|inv| inv.equipped(equip_slot))
903            .and_then(|item| match &*item.kind() {
904                ItemKind::Tool(tool) => Some(Duration::from_secs_f32(
905                    tool.stats(item.stats_durability_multiplier())
906                        .equip_time_secs,
907                )),
908                _ => None,
909            })
910    };
911
912    // Calculates time required to equip weapons, if weapon in mainhand and offhand,
913    // uses maximum duration
914    let mainhand_equip_time = equip_time(EquipSlot::ActiveMainhand);
915    let offhand_equip_time = equip_time(EquipSlot::ActiveOffhand);
916    let equip_time = match (mainhand_equip_time, offhand_equip_time) {
917        (Some(a), Some(b)) => Some(a.max(b)),
918        (Some(a), None) | (None, Some(a)) => Some(a),
919        (None, None) => None,
920    };
921
922    // Moves entity into equipping state if there is some equip time, else moves
923    // instantly into wield
924    if let Some(equip_time) = equip_time {
925        update.character = CharacterState::Equipping(equipping::Data {
926            static_data: equipping::StaticData {
927                buildup_duration: equip_time,
928            },
929            timer: Duration::default(),
930            is_sneaking: update.character.is_stealthy(),
931        });
932    } else {
933        update.character = CharacterState::Wielding(wielding::Data {
934            is_sneaking: update.character.is_stealthy(),
935        });
936    }
937}
938
939/// Checks that player can `Sit` and updates `CharacterState` if so
940pub fn attempt_sit(data: &JoinData<'_>, update: &mut StateUpdate) {
941    if data.physics.on_ground.is_some() {
942        update.character = CharacterState::Sit;
943    }
944}
945
946/// Checks that player can `Crawl` and updates `CharacterState` if so
947pub fn attempt_crawl(data: &JoinData<'_>, update: &mut StateUpdate) {
948    if data.physics.on_ground.is_some() {
949        update.character = CharacterState::Crawl;
950    }
951}
952
953pub fn attempt_dance(data: &JoinData<'_>, update: &mut StateUpdate) {
954    if data.physics.on_ground.is_some() && data.body.is_humanoid() {
955        update.character = CharacterState::Dance;
956    }
957}
958
959pub fn can_perform_pet(position: Pos, target_position: Pos, target_alignment: Alignment) -> bool {
960    let within_distance = position.0.distance_squared(target_position.0) <= MAX_MOUNT_RANGE.powi(2);
961    let valid_alignment = matches!(target_alignment, Alignment::Owned(_) | Alignment::Tame);
962
963    within_distance && valid_alignment
964}
965
966pub fn attempt_talk(data: &JoinData<'_>, update: &mut StateUpdate) {
967    if data.physics.on_ground.is_some() {
968        update.character = CharacterState::Talk(Default::default());
969    }
970}
971
972pub fn attempt_sneak(data: &JoinData<'_>, update: &mut StateUpdate) {
973    if data.physics.on_ground.is_some() && data.body.is_humanoid() {
974        update.character = Idle(idle::Data {
975            is_sneaking: true,
976            time_entered: *data.time,
977            footwear: data.character.footwear(),
978        });
979    }
980}
981
982/// Checks that player can `Climb` and updates `CharacterState` if so
983pub fn handle_climb(data: &JoinData<'_>, update: &mut StateUpdate) -> bool {
984    let Some(wall_dir) = data.physics.on_wall else {
985        return false;
986    };
987
988    let towards_wall = data.inputs.move_dir.dot(wall_dir.xy()) > 0.0;
989    // Only allow climbing if we are near the surface
990    let underwater = data
991        .physics
992        .in_liquid()
993        .map(|depth| depth > 2.0)
994        .unwrap_or(false);
995    let can_climb = data.body.can_climb() || data.physics.in_liquid().is_some();
996    let in_air = data.physics.on_ground.is_none();
997    if towards_wall && in_air && !underwater && can_climb && update.energy.current() > 1.0 {
998        update.character = CharacterState::Climb(
999            climb::Data::create_adjusted_by_skills(data)
1000                .with_wielded(data.character.is_wield() || data.character.was_wielded()),
1001        );
1002        true
1003    } else {
1004        false
1005    }
1006}
1007
1008pub fn handle_wallrun(data: &JoinData<'_>, update: &mut StateUpdate) -> bool {
1009    if data.physics.on_wall.is_some()
1010        && data.physics.on_ground.is_none()
1011        && data.physics.in_liquid().is_none()
1012        && data.body.can_climb()
1013    {
1014        update.character = CharacterState::Wallrun(wallrun::Data {
1015            was_wielded: data.character.is_wield() || data.character.was_wielded(),
1016        });
1017        true
1018    } else {
1019        false
1020    }
1021}
1022/// Checks that player can Swap Weapons and updates `Loadout` if so
1023pub fn attempt_swap_equipped_weapons(
1024    data: &JoinData<'_>,
1025    update: &mut StateUpdate,
1026    output_events: &mut OutputEvents,
1027) {
1028    if data
1029        .inventory
1030        .and_then(|inv| inv.equipped(EquipSlot::InactiveMainhand))
1031        .is_some()
1032        || data
1033            .inventory
1034            .and_then(|inv| inv.equipped(EquipSlot::InactiveOffhand))
1035            .is_some()
1036    {
1037        update.swap_equipped_weapons = true;
1038        loadout_change_hook(data, output_events, false);
1039    }
1040}
1041
1042/// Checks if a block can be reached from a position.
1043fn can_reach_block(
1044    player_pos: Vec3<f32>,
1045    block_pos: Vec3<i32>,
1046    range: f32,
1047    body: &Body,
1048    terrain: &TerrainGrid,
1049) -> bool {
1050    let block_pos_f32 = block_pos.map(|x| x as f32 + 0.5);
1051    // Closure to check if distance between a point and the block is less than
1052    // MAX_PICKUP_RANGE and the radius of the body
1053    let block_range_check = |pos: Vec3<f32>| {
1054        (block_pos_f32 - pos).magnitude_squared() < (range + body.max_radius()).powi(2)
1055    };
1056
1057    // Checks if player's feet or head is near to block
1058    let close_to_block = block_range_check(player_pos)
1059        || block_range_check(player_pos + Vec3::new(0.0, 0.0, body.height()));
1060    if close_to_block {
1061        // Do a check that a path can be found between sprite and entity
1062        // interacting with sprite Use manhattan distance * 1.5 for number
1063        // of iterations
1064        let iters = (3.0 * (block_pos_f32 - player_pos).map(|x| x.abs()).sum()) as usize;
1065        // Heuristic compares manhattan distance of start and end pos
1066        let heuristic = move |pos: &Vec3<i32>| (block_pos - pos).map(|x| x.abs()).sum() as f32;
1067
1068        let mut astar = Astar::new(
1069            iters,
1070            player_pos.map(|x| x.floor() as i32),
1071            BuildHasherDefault::<FxHasher64>::default(),
1072        );
1073
1074        // Transition uses manhattan distance as the cost, with a slightly lower cost
1075        // for z transitions
1076        let transition = |a: Vec3<i32>, b: Vec3<i32>| {
1077            let (a, b) = (a.map(|x| x as f32), b.map(|x| x as f32));
1078            ((a - b) * Vec3::new(1.0, 1.0, 0.9)).map(|e| e.abs()).sum()
1079        };
1080        // Neighbors are all neighboring blocks that are air
1081        let neighbors = |pos: &Vec3<i32>| {
1082            const DIRS: [Vec3<i32>; 6] = [
1083                Vec3::new(1, 0, 0),
1084                Vec3::new(-1, 0, 0),
1085                Vec3::new(0, 1, 0),
1086                Vec3::new(0, -1, 0),
1087                Vec3::new(0, 0, 1),
1088                Vec3::new(0, 0, -1),
1089            ];
1090            let pos = *pos;
1091            DIRS.iter()
1092                .map(move |dir| {
1093                    let dest = dir + pos;
1094                    (dest, transition(pos, dest))
1095                })
1096                .filter(|(pos, _)| {
1097                    terrain
1098                        .get(*pos)
1099                        .ok()
1100                        .is_some_and(|block| !block.is_filled())
1101                })
1102        };
1103        // Pathing satisfied when it reaches the sprite position
1104        let satisfied = |pos: &Vec3<i32>| *pos == block_pos;
1105
1106        astar
1107            .poll(iters, heuristic, neighbors, satisfied)
1108            .into_path()
1109            .is_some()
1110    } else {
1111        false
1112    }
1113}
1114
1115/// Handles inventory manipulations that affect the loadout
1116pub fn handle_manipulate_loadout(
1117    data: &JoinData<'_>,
1118    output_events: &mut OutputEvents,
1119    update: &mut StateUpdate,
1120    inv_action: InventoryAction,
1121) {
1122    loadout_change_hook(data, output_events, true);
1123    match inv_action {
1124        InventoryAction::Use(slot @ Slot::Inventory(inv_slot)) => {
1125            // If inventory action is using a slot, and slot is in the inventory
1126            // TODO: Do some non lazy way of handling the possibility that items equipped in
1127            // the loadout will have effects that are desired to be non-instantaneous
1128            use use_item::ItemUseKind;
1129            if let Some((item_kind, item)) = data
1130                .inventory
1131                .and_then(|inv| inv.get(inv_slot))
1132                .and_then(|item| Option::<ItemUseKind>::from(&*item.kind()).zip(Some(item)))
1133            {
1134                let (buildup_duration, use_duration, recover_duration) = item_kind.durations();
1135                // If item returns a valid kind for item use, do into use item character state
1136                update.character = CharacterState::UseItem(use_item::Data {
1137                    static_data: use_item::StaticData {
1138                        buildup_duration,
1139                        use_duration,
1140                        recover_duration,
1141                        inv_slot,
1142                        item_kind,
1143                        item_hash: item.item_hash(),
1144                        was_wielded: data.character.is_wield(),
1145                        was_sneak: data.character.is_stealthy(),
1146                    },
1147                    timer: Duration::default(),
1148                    stage_section: StageSection::Buildup,
1149                });
1150            } else {
1151                // Else emit inventory action instantaneously
1152                let inv_manip = InventoryManip::Use(slot);
1153                output_events.emit_server(InventoryManipEvent(data.entity, inv_manip));
1154            }
1155        },
1156        InventoryAction::Collect(sprite_pos) => {
1157            // First, get sprite data for position, if there is a sprite
1158            let sprite_at_pos = data
1159                .terrain
1160                .get(sprite_pos)
1161                .ok()
1162                .copied()
1163                .and_then(|b| b.get_sprite());
1164            // Checks if position has a collectible sprite as well as what sprite is at the
1165            // position
1166            let sprite_interact =
1167                sprite_at_pos.and_then(Option::<interact::SpriteInteractKind>::from);
1168            if let Some(sprite_interact) = sprite_interact {
1169                let sprite_chunk_pos = TerrainGrid::chunk_offs(sprite_pos);
1170                let sprite_cfg = data
1171                    .terrain
1172                    .pos_chunk(sprite_pos)
1173                    .and_then(|chunk| chunk.meta().sprite_cfg_at(sprite_chunk_pos));
1174                if can_reach_block(
1175                    data.pos.0,
1176                    sprite_pos,
1177                    MAX_PICKUP_RANGE,
1178                    data.body,
1179                    data.terrain,
1180                ) {
1181                    let required_item =
1182                        sprite_at_pos.and_then(|s| match s.unlock_condition(sprite_cfg.cloned()) {
1183                            UnlockKind::Free => None,
1184                            UnlockKind::Requires(item) => Some((item, false)),
1185                            UnlockKind::Consumes(item) => Some((item, true)),
1186                        });
1187                    // None: An required items exist but no available
1188                    // Some(None): No required items
1189                    // Some(Some(_)): Required items satisfied, contains info about them
1190                    let has_required_items = match required_item {
1191                        // Produces `None` if we can't find the item or `Some(Some(_))` if we can
1192                        Some((item_id, consume)) => data
1193                            .inventory
1194                            .and_then(|inv| inv.get_slot_of_item_by_def_id(&item_id))
1195                            .map(|slot| Some((item_id, slot, consume))),
1196                        None => Some(None),
1197                    };
1198                    if let Some(required_item) = has_required_items {
1199                        // If the sprite is collectible, enter the sprite interaction character
1200                        // state TODO: Handle cases for sprite being
1201                        // interactible, but not collectible (none currently
1202                        // exist)
1203                        let (buildup_duration, use_duration, recover_duration) =
1204                            sprite_interact.durations();
1205
1206                        update.character = CharacterState::Interact(interact::Data {
1207                            static_data: interact::StaticData {
1208                                buildup_duration,
1209                                // Item interactions are never indefinite
1210                                use_duration: Some(use_duration),
1211                                recover_duration,
1212                                interact: interact::InteractKind::Sprite {
1213                                    pos: sprite_pos,
1214                                    kind: sprite_interact,
1215                                },
1216                                was_wielded: data.character.is_wield(),
1217                                was_sneak: data.character.is_stealthy(),
1218                                required_item,
1219                            },
1220                            timer: Duration::default(),
1221                            stage_section: StageSection::Buildup,
1222                        })
1223                    } else {
1224                        output_events.emit_local(LocalEvent::CreateOutcome(
1225                            Outcome::FailedSpriteUnlock { pos: sprite_pos },
1226                        ));
1227                    }
1228                }
1229            }
1230        },
1231        // For inventory actions without a dedicated character state, just do action instantaneously
1232        InventoryAction::Swap(equip, slot) => {
1233            let inv_manip = InventoryManip::Swap(Slot::Equip(equip), slot);
1234            output_events.emit_server(InventoryManipEvent(data.entity, inv_manip));
1235        },
1236        InventoryAction::Drop(equip) => {
1237            let inv_manip = InventoryManip::Drop(Slot::Equip(equip));
1238            output_events.emit_server(InventoryManipEvent(data.entity, inv_manip));
1239        },
1240        InventoryAction::Sort(sort_order) => {
1241            output_events.emit_server(InventoryManipEvent(
1242                data.entity,
1243                InventoryManip::Sort(sort_order),
1244            ));
1245        },
1246        InventoryAction::Use(slot @ Slot::Equip(_)) => {
1247            let inv_manip = InventoryManip::Use(slot);
1248            output_events.emit_server(InventoryManipEvent(data.entity, inv_manip));
1249        },
1250        InventoryAction::Use(Slot::Overflow(_)) => {
1251            // Items in overflow slots cannot be used until moved to a real slot
1252        },
1253        InventoryAction::ToggleSpriteLight(pos, enable) => {
1254            if matches!(pos.kind, Volume::Terrain) {
1255                let sprite_interact = interact::SpriteInteractKind::ToggleLight(enable);
1256
1257                let (buildup_duration, use_duration, recover_duration) =
1258                    sprite_interact.durations();
1259
1260                update.character = CharacterState::Interact(interact::Data {
1261                    static_data: interact::StaticData {
1262                        buildup_duration,
1263                        use_duration: Some(use_duration),
1264                        recover_duration,
1265                        interact: interact::InteractKind::Sprite {
1266                            pos: pos.pos,
1267                            kind: sprite_interact,
1268                        },
1269                        was_wielded: data.character.is_wield(),
1270                        was_sneak: data.character.is_stealthy(),
1271                        required_item: None,
1272                    },
1273                    timer: Duration::default(),
1274                    stage_section: StageSection::Buildup,
1275                });
1276            }
1277        },
1278    }
1279}
1280
1281/// Checks that player can wield the glider and updates `CharacterState` if so
1282pub fn attempt_glide_wield(
1283    data: &JoinData<'_>,
1284    update: &mut StateUpdate,
1285    output_events: &mut OutputEvents,
1286) {
1287    if data
1288        .inventory
1289        .and_then(|inv| inv.equipped(EquipSlot::Glider))
1290        .is_some()
1291        && !data
1292            .physics
1293            .in_liquid()
1294            .map(|depth| depth > 1.0)
1295            .unwrap_or(false)
1296        && data.body.is_humanoid()
1297        && data.mount_data.is_none()
1298        && data.volume_mount_data.is_none()
1299    {
1300        output_events.emit_local(LocalEvent::CreateOutcome(Outcome::Glider {
1301            pos: data.pos.0,
1302            wielded: true,
1303        }));
1304        update.character = CharacterState::GlideWield(glide_wield::Data::from(data));
1305    }
1306}
1307
1308/// Checks that player can jump and sends jump event if so
1309pub fn handle_jump(
1310    data: &JoinData<'_>,
1311    output_events: &mut OutputEvents,
1312    _update: &mut StateUpdate,
1313    strength: f32,
1314) -> bool {
1315    input_is_pressed(data, InputKind::Jump)
1316        .then(|| data.body.jump_impulse())
1317        .flatten()
1318        .and_then(|impulse| {
1319            if data.physics.in_liquid().is_some() {
1320                if data.physics.on_wall.is_some() {
1321                    // Allow entities to make a small jump when at the edge of a body of water,
1322                    // allowing them to path out of it
1323                    Some(impulse * 0.75)
1324                } else {
1325                    None
1326                }
1327            } else if data.physics.on_ground.is_some() {
1328                Some(impulse)
1329            } else {
1330                None
1331            }
1332        })
1333        .map(|impulse| {
1334            output_events.emit_local(LocalEvent::Jump(
1335                data.entity,
1336                strength * impulse / data.mass.0
1337                    * data.scale.map_or(1.0, |s| s.0.powf(13.0).powf(0.25))
1338                    * data.stats.jump_modifier,
1339            ));
1340        })
1341        .is_some()
1342}
1343
1344pub fn handle_walljump(
1345    data: &JoinData<'_>,
1346    output_events: &mut OutputEvents,
1347    update: &mut StateUpdate,
1348    was_wielded: bool,
1349) -> bool {
1350    let Some(wall_dir) = data.physics.on_wall else {
1351        return false;
1352    };
1353    const WALL_JUMP_Z: f32 = 0.7;
1354    let look_dir = data.inputs.look_dir.vec();
1355
1356    // If looking at wall jump into look direction reflected off of the wall
1357    let jump_dir = if look_dir.xy().dot(wall_dir.xy()) > 0.0 {
1358        look_dir.xy().reflected(-wall_dir.xy()).with_z(WALL_JUMP_Z)
1359    } else {
1360        *look_dir
1361    };
1362
1363    // If there is move input while walljumping favour the input direction
1364    let jump_dir = if data.inputs.move_dir.dot(-wall_dir.xy()) > 0.0 {
1365        data.inputs.move_dir.with_z(WALL_JUMP_Z)
1366    } else {
1367        jump_dir
1368    };
1369
1370    // Prevent infinite upwards jumping
1371    let jump_dir = if jump_dir.xy().iter().all(|e| *e < 0.001) {
1372        jump_dir - wall_dir.xy() * 0.1
1373    } else {
1374        jump_dir
1375    }
1376    .try_normalized()
1377    .unwrap_or(Vec3::zero());
1378
1379    if let Some(jump_impulse) = data.body.jump_impulse() {
1380        // Update orientation to look towards jump direction
1381        update.ori = update
1382            .ori
1383            .slerped_towards(Ori::from(Dir::new(jump_dir)), 20.0);
1384        // How strong the climb boost is relative to a normal jump
1385        const WALL_JUMP_FACTOR: f32 = 1.1;
1386        // Apply force
1387        output_events.emit_local(LocalEvent::ApplyImpulse {
1388            entity: data.entity,
1389            impulse: jump_dir * WALL_JUMP_FACTOR * jump_impulse / data.mass.0
1390                * data.scale.map_or(1.0, |s| s.0.powf(13.0).powf(0.25)),
1391        });
1392    }
1393    if was_wielded {
1394        update.character = CharacterState::Wielding(wielding::Data { is_sneaking: false });
1395    } else {
1396        update.character = CharacterState::Idle(idle::Data::default());
1397    }
1398    true
1399}
1400
1401fn handle_ability(
1402    data: &JoinData<'_>,
1403    update: &mut StateUpdate,
1404    output_events: &mut OutputEvents,
1405    input: InputKind,
1406) -> bool {
1407    let context = AbilityContext::from(data.stance, data.inventory, data.combo);
1408    if let Some(ability_input) = input.into() {
1409        if let Some((ability, from_offhand, spec_ability)) = data
1410            .active_abilities
1411            .and_then(|a| {
1412                a.activate_ability(
1413                    ability_input,
1414                    data.inventory,
1415                    data.skill_set,
1416                    Some(data.body),
1417                    Some(data.character),
1418                    &context,
1419                    Some(data.stats),
1420                )
1421            })
1422            .map(|(mut a, f, s)| {
1423                if let Some(contextual_stats) = a.ability_meta().contextual_stats {
1424                    a = a.adjusted_by_stats(contextual_stats.equivalent_stats(data))
1425                }
1426                (a, f, s)
1427            })
1428            .filter(|(ability, _, _)| ability.requirements_paid(data, update))
1429        {
1430            match CharacterState::try_from((
1431                &ability,
1432                AbilityInfo::new(
1433                    data,
1434                    from_offhand,
1435                    input,
1436                    Some(spec_ability),
1437                    ability.ability_meta(),
1438                ),
1439                data,
1440            )) {
1441                Ok(character_state) => {
1442                    update.character = character_state;
1443
1444                    if let Some(init_event) = ability.ability_meta().init_event {
1445                        match init_event {
1446                            AbilityInitEvent::EnterStance(stance) => {
1447                                output_events.emit_server(ChangeStanceEvent {
1448                                    entity: data.entity,
1449                                    stance,
1450                                });
1451                            },
1452                            AbilityInitEvent::GainBuff {
1453                                kind,
1454                                strength,
1455                                duration,
1456                            } => {
1457                                let dest_info = DestInfo {
1458                                    stats: Some(data.stats),
1459                                    mass: Some(data.mass),
1460                                };
1461                                output_events.emit_server(BuffEvent {
1462                                    entity: data.entity,
1463                                    buff_change: BuffChange::Add(Buff::new(
1464                                        kind,
1465                                        BuffData::new(strength, duration),
1466                                        vec![BuffCategory::SelfBuff],
1467                                        BuffSource::Character { by: *data.uid },
1468                                        *data.time,
1469                                        dest_info,
1470                                        Some(data.mass),
1471                                    )),
1472                                });
1473                            },
1474                        }
1475                    }
1476                    if let CharacterState::Roll(roll) = &mut update.character {
1477                        if data.character.is_wield() || data.character.was_wielded() {
1478                            roll.was_wielded = true;
1479                        }
1480                        if data.character.is_stealthy() {
1481                            roll.is_sneaking = true;
1482                        }
1483                        if data.character.is_aimed() {
1484                            roll.prev_aimed_dir = Some(data.controller.inputs.look_dir);
1485                        }
1486                    }
1487                    return true;
1488                },
1489                Err(err) => {
1490                    warn!("Failed to enter character state: {err:?}");
1491                },
1492            }
1493        }
1494    }
1495    false
1496}
1497
1498pub fn handle_input(
1499    data: &JoinData<'_>,
1500    output_events: &mut OutputEvents,
1501    update: &mut StateUpdate,
1502    input: InputKind,
1503) {
1504    match input {
1505        InputKind::Primary
1506        | InputKind::Secondary
1507        | InputKind::Ability(_)
1508        | InputKind::Block
1509        | InputKind::Roll => {
1510            handle_ability(data, update, output_events, input);
1511        },
1512        InputKind::Jump => {
1513            handle_jump(data, output_events, update, 1.0);
1514        },
1515        InputKind::Fly => {},
1516    }
1517}
1518
1519// NOTE: Quality of Life hack
1520//
1521// Uses glider ability if has any, otherwise fallback
1522pub fn handle_glider_input_or(
1523    data: &JoinData<'_>,
1524    update: &mut StateUpdate,
1525    output_events: &mut OutputEvents,
1526    fallback_fn: fn(&JoinData<'_>, &mut StateUpdate),
1527) {
1528    if data
1529        .inventory
1530        .and_then(|inv| inv.equipped(EquipSlot::Glider))
1531        .and_then(|glider| glider.item_config())
1532        .is_none()
1533    {
1534        fallback_fn(data, update);
1535        return;
1536    };
1537
1538    if let Some(input) = data.controller.queued_inputs.keys().next() {
1539        handle_ability(data, update, output_events, *input);
1540    };
1541}
1542
1543pub fn attempt_input(
1544    data: &JoinData<'_>,
1545    output_events: &mut OutputEvents,
1546    update: &mut StateUpdate,
1547) {
1548    // TODO: look into using first() when it becomes stable
1549    if let Some(input) = data.controller.queued_inputs.keys().next() {
1550        handle_input(data, output_events, update, *input);
1551    }
1552}
1553
1554/// Returns whether an interrupt occurred
1555pub fn handle_interrupts(
1556    data: &JoinData,
1557    update: &mut StateUpdate,
1558    output_events: &mut OutputEvents,
1559) -> bool {
1560    let can_dodge = matches!(
1561        data.character.stage_section(),
1562        Some(StageSection::Buildup | StageSection::Recover)
1563    );
1564    let can_block = data
1565        .character
1566        .ability_info()
1567        .map(|info| info.ability_meta)
1568        .is_some_and(|meta| meta.capabilities.contains(Capability::BLOCK_INTERRUPT));
1569    if can_dodge && input_is_pressed(data, InputKind::Roll) {
1570        handle_ability(data, update, output_events, InputKind::Roll)
1571    } else if can_block && input_is_pressed(data, InputKind::Block) {
1572        handle_ability(data, update, output_events, InputKind::Block)
1573    } else {
1574        false
1575    }
1576}
1577
1578pub fn is_strafing(data: &JoinData<'_>, update: &StateUpdate) -> bool {
1579    // TODO: Don't always check `character.is_aimed()`, allow the frontend to
1580    // control whether the player strafes during an aimed `CharacterState`.
1581    (update.character.is_aimed() || update.should_strafe) && data.body.can_strafe()
1582    // no strafe with music instruments equipped in ActiveMainhand
1583    && !matches!(unwrap_tool_data(data, EquipSlot::ActiveMainhand),
1584        Some((ToolKind::Instrument, _)))
1585}
1586
1587/// Returns tool and components
1588pub fn unwrap_tool_data(data: &JoinData, equip_slot: EquipSlot) -> Option<(ToolKind, Hands)> {
1589    if let Some(ItemKind::Tool(tool)) = data
1590        .inventory
1591        .and_then(|inv| inv.equipped(equip_slot))
1592        .map(|i| i.kind())
1593        .as_deref()
1594    {
1595        Some((tool.kind, tool.hands))
1596    } else {
1597        None
1598    }
1599}
1600
1601pub fn get_hands(data: &JoinData<'_>) -> (Option<Hands>, Option<Hands>) {
1602    let hand = |slot| {
1603        if let Some(ItemKind::Tool(tool)) = data
1604            .inventory
1605            .and_then(|inv| inv.equipped(slot))
1606            .map(|i| i.kind())
1607            .as_deref()
1608        {
1609            Some(tool.hands)
1610        } else {
1611            None
1612        }
1613    };
1614    (
1615        hand(EquipSlot::ActiveMainhand),
1616        hand(EquipSlot::ActiveOffhand),
1617    )
1618}
1619
1620pub fn get_tool_stats(data: &JoinData<'_>, ai: AbilityInfo) -> tool::Stats {
1621    ai.hand
1622        .map(|hand| hand.to_equip_slot())
1623        .and_then(|slot| data.inventory.and_then(|inv| inv.equipped(slot)))
1624        .and_then(|item| {
1625            if let ItemKind::Tool(tool) = &*item.kind() {
1626                Some(tool.stats(item.stats_durability_multiplier()))
1627            } else {
1628                None
1629            }
1630        })
1631        .unwrap_or(tool::Stats::one())
1632}
1633
1634pub fn input_is_pressed(data: &JoinData<'_>, input: InputKind) -> bool {
1635    data.controller.queued_inputs.contains_key(&input)
1636}
1637
1638/// Checked `Duration` addition. Computes `timer` + `dt`, only applying
1639/// the explicitly given modifier and returning None if overflow
1640/// occurred.
1641fn checked_tick(data: &JoinData<'_>, timer: Duration, modifier: Option<f32>) -> Option<Duration> {
1642    timer.checked_add(Duration::from_secs_f32(data.dt.0 * modifier.unwrap_or(1.0)))
1643}
1644
1645/// Ticks `timer` by `dt`, only applying the explicitly given modifier.
1646/// Returns `Duration::default()` if overflow occurs
1647pub fn tick_or_default(data: &JoinData<'_>, timer: Duration, modifier: Option<f32>) -> Duration {
1648    checked_tick(data, timer, modifier).unwrap_or_default()
1649}
1650
1651/// Checked `Duration` addition. Computes `timer` + `dt`, applying relevant stat
1652/// attack modifiers and returning None if overflow
1653/// occurred.
1654fn checked_tick_attack(
1655    data: &JoinData<'_>,
1656    timer: Duration,
1657    other_modifier: Option<f32>,
1658) -> Option<Duration> {
1659    checked_tick(
1660        data,
1661        timer,
1662        Some(data.stats.attack_speed_modifier * other_modifier.unwrap_or(1.0)),
1663    )
1664}
1665
1666/// Ticks `timer` by `dt`, applying relevant stat attack modifiers and
1667/// `other_modifier`. Returns `Duration::default()` if overflow occurs
1668pub fn tick_attack_or_default(
1669    data: &JoinData<'_>,
1670    timer: Duration,
1671    other_modifier: Option<f32>,
1672) -> Duration {
1673    checked_tick_attack(data, timer, other_modifier).unwrap_or_default()
1674}
1675
1676/// Determines what portion a state is in. Used in all attacks (eventually). Is
1677/// used to control aspects of animation code, as well as logic within the
1678/// character states.
1679#[derive(Clone, Copy, Debug, Display, Eq, Hash, PartialEq, Serialize, Deserialize)]
1680pub enum StageSection {
1681    Buildup,
1682    Recover,
1683    Charge,
1684    Movement,
1685    Action,
1686}
1687
1688#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
1689pub enum ForcedMovement {
1690    Forward(f32),
1691    Reverse(f32),
1692    Sideways(f32),
1693    DirectedReverse(f32),
1694    AntiDirectedForward(f32),
1695    Leap {
1696        vertical: f32,
1697        forward: f32,
1698        progress: f32,
1699        direction: MovementDirection,
1700    },
1701    Hover {
1702        move_input: f32,
1703    },
1704}
1705
1706impl Mul<f32> for ForcedMovement {
1707    type Output = Self;
1708
1709    fn mul(self, scalar: f32) -> Self {
1710        use ForcedMovement::*;
1711        match self {
1712            Forward(x) => Forward(x * scalar),
1713            Reverse(x) => Reverse(x * scalar),
1714            Sideways(x) => Sideways(x * scalar),
1715            DirectedReverse(x) => DirectedReverse(x * scalar),
1716            AntiDirectedForward(x) => AntiDirectedForward(x * scalar),
1717            Leap {
1718                vertical,
1719                forward,
1720                progress,
1721                direction,
1722            } => Leap {
1723                vertical: vertical * scalar,
1724                forward: forward * scalar,
1725                progress,
1726                direction,
1727            },
1728            Hover { move_input } => Hover { move_input },
1729        }
1730    }
1731}
1732
1733#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1734pub enum MovementDirection {
1735    Look,
1736    Move,
1737}
1738
1739impl MovementDirection {
1740    pub fn get_2d_dir(self, data: &JoinData<'_>) -> Vec2<f32> {
1741        use MovementDirection::*;
1742        match self {
1743            Look => data
1744                .inputs
1745                .look_dir
1746                .to_horizontal()
1747                .unwrap_or_default()
1748                .xy(),
1749            Move => data.inputs.move_dir,
1750        }
1751        .try_normalized()
1752        .unwrap_or_default()
1753    }
1754}
1755
1756#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
1757pub struct AbilityInfo {
1758    pub tool: Option<ToolKind>,
1759    pub hand: Option<HandInfo>,
1760    pub input: InputKind,
1761    pub input_attr: Option<InputAttr>,
1762    pub ability_meta: AbilityMeta,
1763    pub ability: Option<SpecifiedAbility>,
1764}
1765
1766impl AbilityInfo {
1767    pub fn new(
1768        data: &JoinData<'_>,
1769        from_offhand: bool,
1770        input: InputKind,
1771        ability: Option<SpecifiedAbility>,
1772        ability_meta: AbilityMeta,
1773    ) -> Self {
1774        let tool_data = if from_offhand {
1775            unwrap_tool_data(data, EquipSlot::ActiveOffhand)
1776        } else {
1777            unwrap_tool_data(data, EquipSlot::ActiveMainhand)
1778        };
1779        let (tool, hand) = tool_data.map_or((None, None), |(kind, hands)| {
1780            (
1781                Some(kind),
1782                Some(HandInfo::from_main_tool(hands, from_offhand)),
1783            )
1784        });
1785
1786        Self {
1787            tool,
1788            hand,
1789            input,
1790            input_attr: data.controller.queued_inputs.get(&input).copied(),
1791            ability_meta,
1792            ability,
1793        }
1794    }
1795}
1796
1797pub fn end_ability(data: &JoinData<'_>, update: &mut StateUpdate) {
1798    if data.character.is_wield() || data.character.was_wielded() {
1799        update.character = CharacterState::Wielding(wielding::Data {
1800            is_sneaking: data.character.is_stealthy(),
1801        });
1802    } else {
1803        update.character = CharacterState::Idle(idle::Data {
1804            is_sneaking: data.character.is_stealthy(),
1805            footwear: None,
1806            time_entered: *data.time,
1807        });
1808    }
1809    if let CharacterState::Roll(roll) = data.character {
1810        if let Some(dir) = roll.prev_aimed_dir {
1811            update.ori = dir.into();
1812        }
1813    }
1814}
1815
1816pub fn end_melee_ability(data: &JoinData<'_>, update: &mut StateUpdate) {
1817    end_ability(data, update);
1818    data.updater.remove::<Melee>(data.entity);
1819}
1820
1821#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1822pub enum HandInfo {
1823    TwoHanded,
1824    MainHand,
1825    OffHand,
1826}
1827
1828impl HandInfo {
1829    pub fn from_main_tool(tool_hands: Hands, from_offhand: bool) -> Self {
1830        match tool_hands {
1831            Hands::Two => Self::TwoHanded,
1832            Hands::One => {
1833                if from_offhand {
1834                    Self::OffHand
1835                } else {
1836                    Self::MainHand
1837                }
1838            },
1839        }
1840    }
1841
1842    pub fn to_equip_slot(&self) -> EquipSlot {
1843        match self {
1844            HandInfo::TwoHanded | HandInfo::MainHand => EquipSlot::ActiveMainhand,
1845            HandInfo::OffHand => EquipSlot::ActiveOffhand,
1846        }
1847    }
1848}
1849
1850pub fn leave_stance(data: &JoinData<'_>, output_events: &mut OutputEvents) {
1851    if !matches!(data.stance, Some(Stance::None)) {
1852        output_events.emit_server(ChangeStanceEvent {
1853            entity: data.entity,
1854            stance: Stance::None,
1855        });
1856    }
1857}
1858
1859#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1860pub enum ScalingKind {
1861    // Reaches a scaling of 1 when at minimum combo, and a scaling of 2 when at double minimum
1862    // combo
1863    Linear,
1864    // Reaches a scaling of 1 when at minimum combo, and a scaling of 2 when at 4x minimum combo
1865    Sqrt,
1866}
1867
1868impl ScalingKind {
1869    pub fn factor(&self, val: f32, norm: f32) -> f32 {
1870        match self {
1871            Self::Linear => val / norm,
1872            Self::Sqrt => (val / norm).sqrt(),
1873        }
1874    }
1875}
1876
1877#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
1878pub enum ComboConsumption {
1879    #[default]
1880    All,
1881    Half,
1882    Cost,
1883}
1884
1885impl ComboConsumption {
1886    pub fn consume(&self, data: &JoinData, output_events: &mut OutputEvents, cost: u32) {
1887        let combo = data.combo.map_or(0, |c| c.counter());
1888        let to_consume = match self {
1889            Self::All => combo,
1890            Self::Half => combo.div_ceil(2),
1891            Self::Cost => cost,
1892        };
1893        output_events.emit_server(ComboChangeEvent {
1894            entity: data.entity,
1895            change: -(to_consume as i32),
1896        });
1897    }
1898}
1899
1900fn loadout_change_hook(data: &JoinData<'_>, output_events: &mut OutputEvents, clear_combo: bool) {
1901    if clear_combo {
1902        // Reset combo to 0
1903        output_events.emit_server(ComboChangeEvent {
1904            entity: data.entity,
1905            change: -data.combo.map_or(0, |c| c.counter() as i32),
1906        });
1907    }
1908    // Clear any buffs from equipped weapons
1909    output_events.emit_server(BuffEvent {
1910        entity: data.entity,
1911        buff_change: BuffChange::RemoveByCategory {
1912            all_required: vec![BuffCategory::RemoveOnLoadoutChange],
1913            any_required: vec![],
1914            none_required: vec![],
1915        },
1916    });
1917}
1918
1919#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, Default)]
1920#[serde(deny_unknown_fields)]
1921pub struct MovementModifier {
1922    pub buildup: Option<f32>,
1923    pub swing: Option<f32>,
1924    pub recover: Option<f32>,
1925}
1926
1927#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, Default)]
1928#[serde(deny_unknown_fields)]
1929pub struct OrientationModifier {
1930    pub buildup: Option<f32>,
1931    pub swing: Option<f32>,
1932    pub recover: Option<f32>,
1933}