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