1use 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 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 pub fn is_crouching(&self) -> bool {
122 self.synced_data()
123 .is_some_and(EntitySyncedData::is_shift_key_down)
124 }
125
126 pub fn set_crouching(&self, crouching: bool) {
128 self.set_shared_shift_key_down(crouching);
129 }
130
131 #[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 #[must_use]
142 pub fn is_fall_flying(&self) -> bool {
143 LivingEntity::is_fall_flying(self)
144 }
145
146 #[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 pub fn set_fall_flying(&self, fall_flying: bool) {
158 LivingEntity::set_fall_flying(self, fall_flying);
159 }
160
161 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 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}