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