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 {
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 pub fn is_crouching(&self) -> bool {
123 self.synced_data()
124 .is_some_and(EntitySyncedData::is_shift_key_down)
125 }
126
127 pub fn wants_to_stop_riding(&self) -> bool {
129 self.synced_data()
130 .is_some_and(EntitySyncedData::is_shift_key_down)
131 }
132
133 pub fn set_crouching(&self, crouching: bool) {
135 self.set_shared_shift_key_down(crouching);
136 }
137
138 #[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 #[must_use]
149 pub fn is_fall_flying(&self) -> bool {
150 LivingEntity::is_fall_flying(self)
151 }
152
153 #[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 pub fn set_fall_flying(&self, fall_flying: bool) {
165 LivingEntity::set_fall_flying(self, fall_flying);
166 }
167
168 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 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}