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