veloren_common/states/
utils.rs

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