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