veloren_common/states/
utils.rs

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