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    /// Returns true if the player wants to stop riding (is holding the shift key)
127    pub fn wants_to_stop_riding(&self) -> bool {
128        self.synced_data()
129            .is_some_and(EntitySyncedData::is_shift_key_down)
130    }
131
132    /// Sets whether the player is shifting (sneaking).
133    pub fn set_crouching(&self, crouching: bool) {
134        self.set_shared_shift_key_down(crouching);
135    }
136
137    /// Returns true if vanilla player rules consider the player swimming.
138    #[must_use]
139    pub fn is_swimming(&self) -> bool {
140        self.synced_data()
141            .is_some_and(EntitySyncedData::is_swimming)
142            && !self.is_flying()
143            && self.game_mode() != GameType::Spectator
144    }
145
146    /// Returns true if the player is currently fall flying (elytra).
147    #[must_use]
148    pub fn is_fall_flying(&self) -> bool {
149        LivingEntity::is_fall_flying(self)
150    }
151
152    /// Returns true if vanilla rules consider this player to be on a climbable block.
153    #[must_use]
154    pub(super) fn on_climbable(&self) -> bool {
155        if self.is_flying() {
156            return false;
157        }
158
159        self.default_living_on_climbable()
160    }
161
162    /// Sets the player's fall flying state.
163    pub fn set_fall_flying(&self, fall_flying: bool) {
164        LivingEntity::set_fall_flying(self, fall_flying);
165    }
166
167    /// Determines the desired pose based on current player state.
168    /// Priority: `Sleeping` > `Swimming` > `FallFlying` > `Sneaking` > `Standing`
169    // TODO: Add SpinAttack pose (requires riptide trident)
170    pub(super) fn get_desired_pose(&self) -> EntityPose {
171        if self.is_sleeping() {
172            EntityPose::Sleeping
173        } else if self.is_swimming() {
174            EntityPose::Swimming
175        } else if self.is_fall_flying() {
176            EntityPose::FallFlying
177        } else if self.is_crouching() && !self.is_flying() {
178            EntityPose::Sneaking
179        } else {
180            EntityPose::Standing
181        }
182    }
183
184    /// Updates the player's pose in entity data based on current state.
185    pub(super) fn update_pose(&self) {
186        if !self.can_player_fit_within_blocks_and_entities_when(EntityPose::Swimming) {
187            return;
188        }
189
190        let desired_pose = self.get_desired_pose();
191        let is_spectator = self.game_mode() == GameType::Spectator;
192        let fits_desired_pose =
193            is_spectator || self.can_player_fit_within_blocks_and_entities_when(desired_pose);
194        let fits_crouching = !fits_desired_pose
195            && self.can_player_fit_within_blocks_and_entities_when(EntityPose::Sneaking);
196
197        let Some(actual_pose) = select_actual_pose(
198            desired_pose,
199            PoseFit {
200                spectator: is_spectator,
201                passenger: self.is_passenger(),
202                desired_pose: fits_desired_pose,
203                crouching: fits_crouching,
204                swimming: true,
205            },
206        ) else {
207            return;
208        };
209
210        self.base.set_pose_and_dimensions(
211            actual_pose,
212            <Self as Entity>::dimensions_for_pose(self, actual_pose),
213        );
214        self.entity_data.lock().base_mut().pose.set(actual_pose);
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use steel_registry::entity_type::EntityAttachment;
221
222    use crate::entity::{SwimmingEnvironment, select_swimming_state};
223
224    use super::*;
225
226    fn assert_vec3_close(left: glam::DVec3, right: glam::DVec3) {
227        let diff = left - right;
228        assert!(
229            diff.length_squared() < 1.0e-12,
230            "expected {left:?} to equal {right:?}"
231        );
232    }
233
234    #[test]
235    fn player_pose_dimensions_match_vanilla_avatar() {
236        assert_eq!(
237            Player::dimensions_for_pose(EntityPose::Standing),
238            PLAYER_STANDING_DIMENSIONS
239        );
240        assert_eq!(
241            Player::dimensions_for_pose(EntityPose::Sneaking),
242            PLAYER_CROUCHING_DIMENSIONS
243        );
244        assert_eq!(
245            Player::dimensions_for_pose(EntityPose::FallFlying),
246            EntityDimensions::new(0.6, 0.6, 0.4)
247        );
248        assert_eq!(
249            Player::dimensions_for_pose(EntityPose::Swimming),
250            EntityDimensions::new(0.6, 0.6, 0.4)
251        );
252        assert_eq!(
253            Player::dimensions_for_pose(EntityPose::SpinAttack),
254            EntityDimensions::new(0.6, 0.6, 0.4)
255        );
256        assert_eq!(
257            Player::dimensions_for_pose(EntityPose::Sleeping),
258            EntityDimensions::new(0.2, 0.2, 0.2)
259        );
260        assert_eq!(
261            Player::dimensions_for_pose(EntityPose::Dying),
262            EntityDimensions::new(0.2, 0.2, 1.62)
263        );
264    }
265
266    #[test]
267    fn player_pose_dimensions_preserve_vanilla_vehicle_attachment() {
268        let standing = Player::dimensions_for_pose(EntityPose::Standing);
269        let crouching = Player::dimensions_for_pose(EntityPose::Sneaking);
270        let swimming = Player::dimensions_for_pose(EntityPose::Swimming);
271
272        assert_vec3_close(
273            standing
274                .attachments
275                .get_clamped(EntityAttachment::Vehicle, 0, 0.0, standing),
276            glam::DVec3::new(0.0, 0.6, 0.0),
277        );
278        assert_vec3_close(
279            crouching
280                .attachments
281                .get_clamped(EntityAttachment::Vehicle, 0, 0.0, crouching),
282            glam::DVec3::new(0.0, 0.6, 0.0),
283        );
284        assert_vec3_close(
285            swimming
286                .attachments
287                .get_clamped(EntityAttachment::Vehicle, 0, 0.0, swimming),
288            glam::DVec3::ZERO,
289        );
290    }
291
292    #[test]
293    fn swimming_state_continues_while_sprinting_in_water() {
294        assert!(select_swimming_state(
295            true,
296            SwimmingEnvironment {
297                sprinting: true,
298                passenger: false,
299                in_water: true,
300                under_water: false,
301                block_fluid_is_water: false,
302            },
303        ));
304    }
305
306    #[test]
307    fn swimming_state_stops_when_current_swimmer_stops_sprinting() {
308        assert!(!select_swimming_state(
309            true,
310            SwimmingEnvironment {
311                sprinting: false,
312                passenger: false,
313                in_water: true,
314                under_water: true,
315                block_fluid_is_water: true,
316            },
317        ));
318    }
319
320    #[test]
321    fn swimming_state_starts_when_sprinting_underwater_in_water_block() {
322        assert!(select_swimming_state(
323            false,
324            SwimmingEnvironment {
325                sprinting: true,
326                passenger: false,
327                in_water: true,
328                under_water: true,
329                block_fluid_is_water: true,
330            },
331        ));
332    }
333
334    #[test]
335    fn swimming_state_does_not_start_from_body_water_only() {
336        assert!(!select_swimming_state(
337            false,
338            SwimmingEnvironment {
339                sprinting: true,
340                passenger: false,
341                in_water: true,
342                under_water: false,
343                block_fluid_is_water: true,
344            },
345        ));
346    }
347
348    #[test]
349    fn swimming_state_stops_while_passenger() {
350        assert!(!select_swimming_state(
351            true,
352            SwimmingEnvironment {
353                sprinting: true,
354                passenger: true,
355                in_water: true,
356                under_water: true,
357                block_fluid_is_water: true,
358            },
359        ));
360    }
361
362    #[test]
363    fn player_pose_selection_keeps_pose_when_swimming_cannot_fit() {
364        assert_eq!(
365            select_actual_pose(
366                EntityPose::Standing,
367                PoseFit {
368                    spectator: false,
369                    passenger: false,
370                    desired_pose: true,
371                    crouching: true,
372                    swimming: false,
373                },
374            ),
375            None
376        );
377    }
378
379    #[test]
380    fn player_pose_selection_allows_spectator_desired_pose() {
381        assert_eq!(
382            select_actual_pose(
383                EntityPose::Standing,
384                PoseFit {
385                    spectator: true,
386                    passenger: false,
387                    desired_pose: false,
388                    crouching: false,
389                    swimming: true,
390                },
391            ),
392            Some(EntityPose::Standing)
393        );
394    }
395
396    #[test]
397    fn player_pose_selection_allows_passenger_desired_pose() {
398        assert_eq!(
399            select_actual_pose(
400                EntityPose::Standing,
401                PoseFit {
402                    spectator: false,
403                    passenger: true,
404                    desired_pose: false,
405                    crouching: false,
406                    swimming: true,
407                },
408            ),
409            Some(EntityPose::Standing)
410        );
411    }
412
413    #[test]
414    fn player_pose_selection_falls_back_to_crouching_when_desired_pose_is_blocked() {
415        assert_eq!(
416            select_actual_pose(
417                EntityPose::Standing,
418                PoseFit {
419                    spectator: false,
420                    passenger: false,
421                    desired_pose: false,
422                    crouching: true,
423                    swimming: true,
424                },
425            ),
426            Some(EntityPose::Sneaking)
427        );
428    }
429
430    #[test]
431    fn player_pose_selection_falls_back_to_swimming_when_crouching_is_blocked() {
432        assert_eq!(
433            select_actual_pose(
434                EntityPose::Standing,
435                PoseFit {
436                    spectator: false,
437                    passenger: false,
438                    desired_pose: false,
439                    crouching: false,
440                    swimming: true,
441                },
442            ),
443            Some(EntityPose::Swimming)
444        );
445    }
446}