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