Skip to main content

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