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