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