Skip to main content

steel_core/player/
entity_state.rs

1//! Core entity state flags for a player.
2//!
3//! Groups player pose and shared-flag helpers.
4
5use steel_registry::entity_data::EntityPose;
6use steel_registry::entity_type::{EntityAttachmentPoint, EntityAttachments, EntityDimensions};
7use steel_utils::WorldAabb;
8use steel_utils::types::GameType;
9
10use crate::behavior::{BlockCollisionContext, blocks::PowderSnowBlock};
11use crate::entity::{Entity, EntitySyncedData, LivingEntity};
12use crate::physics::{CollisionWorld, WorldCollisionProvider};
13use crate::player::Player;
14
15const POSE_COLLISION_EPSILON: f64 = 1.0E-7;
16const PLAYER_VEHICLE_ATTACHMENT: [EntityAttachmentPoint; 1] =
17    [EntityAttachmentPoint::new(0.0, 0.6, 0.0)];
18const NO_ATTACHMENT_POINTS: [EntityAttachmentPoint; 0] = [];
19
20const fn player_dimensions_with_vehicle_attachment(
21    width: f32,
22    height: f32,
23    eye_height: f32,
24) -> EntityDimensions {
25    EntityDimensions::new_with_attachments(
26        width,
27        height,
28        eye_height,
29        EntityAttachments::new(
30            &NO_ATTACHMENT_POINTS,
31            &PLAYER_VEHICLE_ATTACHMENT,
32            &NO_ATTACHMENT_POINTS,
33            &NO_ATTACHMENT_POINTS,
34        ),
35    )
36}
37
38const PLAYER_STANDING_DIMENSIONS: EntityDimensions =
39    player_dimensions_with_vehicle_attachment(0.6, 1.8, 1.62);
40const PLAYER_CROUCHING_DIMENSIONS: EntityDimensions =
41    player_dimensions_with_vehicle_attachment(0.6, 1.5, 1.27);
42const PLAYER_SWIMMING_DIMENSIONS: EntityDimensions = EntityDimensions::new(0.6, 0.6, 0.4);
43const PLAYER_SLEEPING_DIMENSIONS: EntityDimensions = EntityDimensions::new(0.2, 0.2, 0.2);
44const PLAYER_DYING_DIMENSIONS: EntityDimensions = EntityDimensions::new(0.2, 0.2, 1.62);
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47struct PoseFit {
48    spectator: bool,
49    passenger: bool,
50    desired_pose: bool,
51    crouching: bool,
52    swimming: bool,
53}
54
55#[must_use]
56const fn select_actual_pose(desired_pose: EntityPose, fit: PoseFit) -> Option<EntityPose> {
57    if !fit.swimming {
58        return None;
59    }
60
61    if fit.spectator || fit.passenger || fit.desired_pose {
62        Some(desired_pose)
63    } else if fit.crouching {
64        Some(EntityPose::Sneaking)
65    } else {
66        Some(EntityPose::Swimming)
67    }
68}
69
70impl Player {
71    /// Returns vanilla `Avatar.POSES` dimensions for a player pose.
72    pub(super) const fn dimensions_for_pose(pose: EntityPose) -> EntityDimensions {
73        match pose {
74            EntityPose::Sleeping => PLAYER_SLEEPING_DIMENSIONS,
75            EntityPose::FallFlying | EntityPose::Swimming | EntityPose::SpinAttack => {
76                PLAYER_SWIMMING_DIMENSIONS
77            }
78            EntityPose::Sneaking => PLAYER_CROUCHING_DIMENSIONS,
79            EntityPose::Dying => PLAYER_DYING_DIMENSIONS,
80            _ => PLAYER_STANDING_DIMENSIONS,
81        }
82    }
83
84    #[must_use]
85    fn bounding_box_for_pose(&self, pose: EntityPose) -> WorldAabb {
86        let position = self.base.position();
87        let dimensions = <Self as Entity>::dimensions_for_pose(self, pose);
88        WorldAabb::entity_box(
89            position.x,
90            position.y,
91            position.z,
92            f64::from(dimensions.half_width()),
93            f64::from(dimensions.height),
94        )
95    }
96
97    #[must_use]
98    fn can_player_fit_within_blocks_and_entities_when(&self, pose: EntityPose) -> bool {
99        let world = self.get_world();
100        let collision_world = WorldCollisionProvider::for_entity(&world, self);
101        !collision_world.has_collision_with_context(
102            &self
103                .bounding_box_for_pose(pose)
104                .deflate(POSE_COLLISION_EPSILON),
105            BlockCollisionContext::entity(self.position().y, self.is_descending())
106                .with_can_walk_on_powder_snow(PowderSnowBlock::can_entity_walk_on_powder_snow(
107                    self,
108                )),
109        )
110    }
111
112    pub(super) fn reset_entity_state(&self) {
113        self.set_shared_swimming(false);
114        self.set_shared_shift_key_down(false);
115        self.clear_sleeping_pos();
116        self.set_fall_flying(false);
117        self.set_sprinting(false);
118    }
119
120    /// Returns true if the player is shifting (sneaking).
121    pub fn is_crouching(&self) -> bool {
122        self.synced_data()
123            .is_some_and(EntitySyncedData::is_shift_key_down)
124    }
125
126    /// Sets whether the player is shifting (sneaking).
127    pub fn set_crouching(&self, crouching: bool) {
128        self.set_shared_shift_key_down(crouching);
129    }
130
131    /// Returns true if vanilla player rules consider the player swimming.
132    #[must_use]
133    pub fn is_swimming(&self) -> bool {
134        self.synced_data()
135            .is_some_and(EntitySyncedData::is_swimming)
136            && !self.is_flying()
137            && self.game_mode() != GameType::Spectator
138    }
139
140    /// Returns true if the player is currently fall flying (elytra).
141    #[must_use]
142    pub fn is_fall_flying(&self) -> bool {
143        LivingEntity::is_fall_flying(self)
144    }
145
146    /// Returns true if vanilla rules consider this player to be on a climbable block.
147    #[must_use]
148    pub(super) fn on_climbable(&self) -> bool {
149        if self.is_flying() {
150            return false;
151        }
152
153        self.default_living_on_climbable()
154    }
155
156    /// Sets the player's fall flying state.
157    pub fn set_fall_flying(&self, fall_flying: bool) {
158        LivingEntity::set_fall_flying(self, fall_flying);
159    }
160
161    /// Determines the desired pose based on current player state.
162    /// Priority: `Sleeping` > `Swimming` > `FallFlying` > `Sneaking` > `Standing`
163    // TODO: Add SpinAttack pose (requires riptide trident)
164    pub(super) fn get_desired_pose(&self) -> EntityPose {
165        if self.is_sleeping() {
166            EntityPose::Sleeping
167        } else if self.is_swimming() {
168            EntityPose::Swimming
169        } else if self.is_fall_flying() {
170            EntityPose::FallFlying
171        } else if self.is_crouching() && !self.is_flying() {
172            EntityPose::Sneaking
173        } else {
174            EntityPose::Standing
175        }
176    }
177
178    /// Updates the player's pose in entity data based on current state.
179    pub(super) fn update_pose(&self) {
180        if !self.can_player_fit_within_blocks_and_entities_when(EntityPose::Swimming) {
181            return;
182        }
183
184        let desired_pose = self.get_desired_pose();
185        let is_spectator = self.game_mode() == GameType::Spectator;
186        let fits_desired_pose =
187            is_spectator || self.can_player_fit_within_blocks_and_entities_when(desired_pose);
188        let fits_crouching = !fits_desired_pose
189            && self.can_player_fit_within_blocks_and_entities_when(EntityPose::Sneaking);
190
191        let Some(actual_pose) = select_actual_pose(
192            desired_pose,
193            PoseFit {
194                spectator: is_spectator,
195                passenger: self.is_passenger(),
196                desired_pose: fits_desired_pose,
197                crouching: fits_crouching,
198                swimming: true,
199            },
200        ) else {
201            return;
202        };
203
204        self.base.set_pose_and_dimensions(
205            actual_pose,
206            <Self as Entity>::dimensions_for_pose(self, actual_pose),
207        );
208        self.entity_data.lock().base_mut().pose.set(actual_pose);
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use steel_registry::entity_type::EntityAttachment;
215
216    use crate::entity::{SwimmingEnvironment, select_swimming_state};
217
218    use super::*;
219
220    fn assert_vec3_close(left: glam::DVec3, right: glam::DVec3) {
221        let diff = left - right;
222        assert!(
223            diff.length_squared() < 1.0e-12,
224            "expected {left:?} to equal {right:?}"
225        );
226    }
227
228    #[test]
229    fn player_pose_dimensions_match_vanilla_avatar() {
230        assert_eq!(
231            Player::dimensions_for_pose(EntityPose::Standing),
232            PLAYER_STANDING_DIMENSIONS
233        );
234        assert_eq!(
235            Player::dimensions_for_pose(EntityPose::Sneaking),
236            PLAYER_CROUCHING_DIMENSIONS
237        );
238        assert_eq!(
239            Player::dimensions_for_pose(EntityPose::FallFlying),
240            EntityDimensions::new(0.6, 0.6, 0.4)
241        );
242        assert_eq!(
243            Player::dimensions_for_pose(EntityPose::Swimming),
244            EntityDimensions::new(0.6, 0.6, 0.4)
245        );
246        assert_eq!(
247            Player::dimensions_for_pose(EntityPose::SpinAttack),
248            EntityDimensions::new(0.6, 0.6, 0.4)
249        );
250        assert_eq!(
251            Player::dimensions_for_pose(EntityPose::Sleeping),
252            EntityDimensions::new(0.2, 0.2, 0.2)
253        );
254        assert_eq!(
255            Player::dimensions_for_pose(EntityPose::Dying),
256            EntityDimensions::new(0.2, 0.2, 1.62)
257        );
258    }
259
260    #[test]
261    fn player_pose_dimensions_preserve_vanilla_vehicle_attachment() {
262        let standing = Player::dimensions_for_pose(EntityPose::Standing);
263        let crouching = Player::dimensions_for_pose(EntityPose::Sneaking);
264        let swimming = Player::dimensions_for_pose(EntityPose::Swimming);
265
266        assert_vec3_close(
267            standing
268                .attachments
269                .get_clamped(EntityAttachment::Vehicle, 0, 0.0, standing),
270            glam::DVec3::new(0.0, 0.6, 0.0),
271        );
272        assert_vec3_close(
273            crouching
274                .attachments
275                .get_clamped(EntityAttachment::Vehicle, 0, 0.0, crouching),
276            glam::DVec3::new(0.0, 0.6, 0.0),
277        );
278        assert_vec3_close(
279            swimming
280                .attachments
281                .get_clamped(EntityAttachment::Vehicle, 0, 0.0, swimming),
282            glam::DVec3::ZERO,
283        );
284    }
285
286    #[test]
287    fn swimming_state_continues_while_sprinting_in_water() {
288        assert!(select_swimming_state(
289            true,
290            SwimmingEnvironment {
291                sprinting: true,
292                passenger: false,
293                in_water: true,
294                under_water: false,
295                block_fluid_is_water: false,
296            },
297        ));
298    }
299
300    #[test]
301    fn swimming_state_stops_when_current_swimmer_stops_sprinting() {
302        assert!(!select_swimming_state(
303            true,
304            SwimmingEnvironment {
305                sprinting: false,
306                passenger: false,
307                in_water: true,
308                under_water: true,
309                block_fluid_is_water: true,
310            },
311        ));
312    }
313
314    #[test]
315    fn swimming_state_starts_when_sprinting_underwater_in_water_block() {
316        assert!(select_swimming_state(
317            false,
318            SwimmingEnvironment {
319                sprinting: true,
320                passenger: false,
321                in_water: true,
322                under_water: true,
323                block_fluid_is_water: true,
324            },
325        ));
326    }
327
328    #[test]
329    fn swimming_state_does_not_start_from_body_water_only() {
330        assert!(!select_swimming_state(
331            false,
332            SwimmingEnvironment {
333                sprinting: true,
334                passenger: false,
335                in_water: true,
336                under_water: false,
337                block_fluid_is_water: true,
338            },
339        ));
340    }
341
342    #[test]
343    fn swimming_state_stops_while_passenger() {
344        assert!(!select_swimming_state(
345            true,
346            SwimmingEnvironment {
347                sprinting: true,
348                passenger: true,
349                in_water: true,
350                under_water: true,
351                block_fluid_is_water: true,
352            },
353        ));
354    }
355
356    #[test]
357    fn player_pose_selection_keeps_pose_when_swimming_cannot_fit() {
358        assert_eq!(
359            select_actual_pose(
360                EntityPose::Standing,
361                PoseFit {
362                    spectator: false,
363                    passenger: false,
364                    desired_pose: true,
365                    crouching: true,
366                    swimming: false,
367                },
368            ),
369            None
370        );
371    }
372
373    #[test]
374    fn player_pose_selection_allows_spectator_desired_pose() {
375        assert_eq!(
376            select_actual_pose(
377                EntityPose::Standing,
378                PoseFit {
379                    spectator: true,
380                    passenger: false,
381                    desired_pose: false,
382                    crouching: false,
383                    swimming: true,
384                },
385            ),
386            Some(EntityPose::Standing)
387        );
388    }
389
390    #[test]
391    fn player_pose_selection_allows_passenger_desired_pose() {
392        assert_eq!(
393            select_actual_pose(
394                EntityPose::Standing,
395                PoseFit {
396                    spectator: false,
397                    passenger: true,
398                    desired_pose: false,
399                    crouching: false,
400                    swimming: true,
401                },
402            ),
403            Some(EntityPose::Standing)
404        );
405    }
406
407    #[test]
408    fn player_pose_selection_falls_back_to_crouching_when_desired_pose_is_blocked() {
409        assert_eq!(
410            select_actual_pose(
411                EntityPose::Standing,
412                PoseFit {
413                    spectator: false,
414                    passenger: false,
415                    desired_pose: false,
416                    crouching: true,
417                    swimming: true,
418                },
419            ),
420            Some(EntityPose::Sneaking)
421        );
422    }
423
424    #[test]
425    fn player_pose_selection_falls_back_to_swimming_when_crouching_is_blocked() {
426        assert_eq!(
427            select_actual_pose(
428                EntityPose::Standing,
429                PoseFit {
430                    spectator: false,
431                    passenger: false,
432                    desired_pose: false,
433                    crouching: false,
434                    swimming: true,
435                },
436            ),
437            Some(EntityPose::Swimming)
438        );
439    }
440}