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 wants_to_stop_riding(&self) -> bool {
128 self.synced_data()
129 .is_some_and(EntitySyncedData::is_shift_key_down)
130 }
131
132 pub fn set_crouching(&self, crouching: bool) {
134 self.set_shared_shift_key_down(crouching);
135 }
136
137 #[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 #[must_use]
148 pub fn is_fall_flying(&self) -> bool {
149 LivingEntity::is_fall_flying(self)
150 }
151
152 #[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 pub fn set_fall_flying(&self, fall_flying: bool) {
164 LivingEntity::set_fall_flying(self, fall_flying);
165 }
166
167 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 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}