steel_core/behavior/blocks/building/
powder_snow_block.rs1use std::sync::Arc;
2
3use glam::DVec3;
4use steel_macros::block_behavior;
5use steel_registry::blocks::{BlockRef, block_state_ext::BlockStateExt as _, shapes::VoxelShape};
6use steel_registry::sound_event::SoundEventRef;
7use steel_registry::vanilla_entity_type_tags::EntityTypeTag;
8use steel_registry::{
9 REGISTRY, TaggedRegistryExt, vanilla_entities, vanilla_game_rules, vanilla_items,
10};
11use steel_utils::{BlockLocalAabb, BlockPos, BlockStateId};
12
13use crate::{
14 behavior::{
15 BlockBehavior, BlockCollisionContext, BlockPlaceContext, EntityFallDamage,
16 EntityFallOnContext,
17 },
18 entity::ai::path::PathComputationType,
19 entity::{Entity, InsideBlockEffectCollector, InsideBlockEffectType},
20 inventory::equipment::EquipmentSlot,
21 world::{LevelReader, World},
22};
23
24const IN_BLOCK_SPEED_MULTIPLIER: DVec3 = DVec3::new(0.9, 1.5, 0.9);
25const NUM_BLOCKS_TO_FALL_INTO_BLOCK: f64 = 2.5;
26const MIN_FALL_DISTANCE_FOR_SOUND: f64 = 4.0;
27const MIN_FALL_DISTANCE_FOR_BIG_SOUND: f64 = 7.0;
28const FALLING_COLLISION_BOXES: &[BlockLocalAabb] =
29 &[BlockLocalAabb::new(0.0, 0.0, 0.0, 1.0, 0.9, 1.0)];
30const FALLING_COLLISION_SHAPE: VoxelShape = VoxelShape::from_boxes(FALLING_COLLISION_BOXES);
31
32#[block_behavior]
38pub struct PowderSnowBlock {
39 block: BlockRef,
40}
41
42impl PowderSnowBlock {
43 #[must_use]
45 pub const fn new(block: BlockRef) -> Self {
46 Self { block }
47 }
48
49 pub(crate) fn can_entity_walk_on_powder_snow<E: Entity + ?Sized>(entity: &E) -> bool {
51 if REGISTRY.entity_types.is_in_tag(
52 entity.entity_type(),
53 &EntityTypeTag::POWDER_SNOW_WALKABLE_MOBS,
54 ) {
55 return true;
56 }
57
58 let Some(living) = entity.as_living_entity() else {
59 return false;
60 };
61 let mut has_leather_boots = false;
62 living.with_equipment_slot(EquipmentSlot::Feet, &mut |item_stack| {
63 has_leather_boots = item_stack.is(&vanilla_items::LEATHER_BOOTS);
64 });
65 has_leather_boots
66 }
67
68 #[must_use]
69 fn fall_sound(context: EntityFallOnContext<'_>) -> Option<SoundEventRef> {
70 if context.fall_distance < MIN_FALL_DISTANCE_FOR_SOUND || !context.entity.is_living_entity {
71 return None;
72 }
73
74 let (small, big) = context.entity.fall_sounds;
75 Some(if context.fall_distance < MIN_FALL_DISTANCE_FOR_BIG_SOUND {
76 small
77 } else {
78 big
79 })
80 }
81}
82
83impl BlockBehavior for PowderSnowBlock {
84 fn get_state_for_placement(&self, _context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
85 Some(self.block.default_state())
86 }
87
88 fn is_pathfindable(
89 &self,
90 _state: BlockStateId,
91 _computation_type: PathComputationType,
92 ) -> bool {
93 true
94 }
95
96 fn fall_on(
97 &self,
98 _state: BlockStateId,
99 _world: &Arc<World>,
100 _pos: BlockPos,
101 context: EntityFallOnContext<'_>,
102 ) -> Option<EntityFallDamage> {
103 if let Some(sound) = Self::fall_sound(context)
104 && let Some(entity) = context.source_entity()
105 {
106 entity.play_sound(sound, 1.0, 1.0);
107 }
108
109 None
110 }
111
112 fn get_entity_inside_collision_shape(
113 &self,
114 state: BlockStateId,
115 world: &dyn LevelReader,
116 pos: BlockPos,
117 entity: &dyn Entity,
118 ) -> VoxelShape {
119 let collision_shape = self.get_collision_shape(
120 state,
121 world,
122 pos,
123 BlockCollisionContext::entity(entity.position().y, entity.is_descending())
124 .with_fall_distance(entity.fall_distance())
125 .with_can_walk_on_powder_snow(Self::can_entity_walk_on_powder_snow(entity))
126 .with_falling_block(entity.entity_type() == &vanilla_entities::FALLING_BLOCK),
127 );
128 if collision_shape.is_empty() {
129 self.default_get_entity_inside_collision_shape(state, world, pos, entity)
130 } else {
131 collision_shape
132 }
133 }
134
135 fn get_collision_shape(
136 &self,
137 state: BlockStateId,
138 world: &dyn LevelReader,
139 pos: BlockPos,
140 context: BlockCollisionContext,
141 ) -> VoxelShape {
142 if context.is_placement() {
143 return VoxelShape::EMPTY;
144 }
145 if context.fall_distance() > NUM_BLOCKS_TO_FALL_INTO_BLOCK {
146 return FALLING_COLLISION_SHAPE;
147 }
148 if context.is_falling_block()
149 || (context.can_walk_on_powder_snow()
150 && context.is_above(VoxelShape::FULL_BLOCK, pos, false)
151 && !context.is_descending())
152 {
153 return self.default_get_collision_shape(state, world, pos, context);
154 }
155
156 VoxelShape::EMPTY
157 }
158
159 fn entity_inside(
160 &self,
161 state: BlockStateId,
162 world: &Arc<World>,
163 pos: BlockPos,
164 entity: &dyn Entity,
165 effect_collector: &mut InsideBlockEffectCollector,
166 _is_precise: bool,
167 ) {
168 if !entity.is_living_entity() || entity.in_block_state(world).get_block() == self.block {
169 entity.make_stuck_in_block(state, IN_BLOCK_SPEED_MULTIPLIER);
170 }
171
172 let world = Arc::clone(world);
173 effect_collector.run_before(
174 InsideBlockEffectType::Extinguish,
175 Box::new(move |entity| {
176 if !entity.is_on_fire() {
177 return;
178 }
179
180 let mob_griefing = world.get_game_rule(&vanilla_game_rules::MOB_GRIEFING);
181 if (mob_griefing || entity.entity_type() == &vanilla_entities::PLAYER)
182 && entity.may_interact(world.as_ref(), pos)
183 {
184 world.destroy_block(pos, false);
185 }
186 }),
187 );
188 effect_collector.apply(InsideBlockEffectType::Freeze);
189 effect_collector.apply(InsideBlockEffectType::Extinguish);
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 use steel_registry::{init_vanilla_registry, sound_events, vanilla_blocks, vanilla_entities};
198
199 use crate::behavior::EntityFallOnFacts;
200 use crate::test_support::TestLevel;
201
202 fn empty_level() -> TestLevel {
203 TestLevel::default().with_min_y(0)
204 }
205
206 fn powder_snow() -> PowderSnowBlock {
207 PowderSnowBlock::new(&vanilla_blocks::POWDER_SNOW)
208 }
209
210 fn powder_snow_state() -> BlockStateId {
211 vanilla_blocks::POWDER_SNOW.default_state()
212 }
213
214 fn fall_context(fall_distance: f64, is_living_entity: bool) -> EntityFallOnContext<'static> {
215 EntityFallOnContext::new(
216 fall_distance,
217 false,
218 EntityFallOnFacts::new(
219 &vanilla_entities::PLAYER,
220 is_living_entity,
221 0.6,
222 1.8,
223 (
224 &sound_events::ENTITY_PLAYER_SMALL_FALL,
225 &sound_events::ENTITY_PLAYER_BIG_FALL,
226 ),
227 ),
228 None,
229 )
230 }
231
232 #[test]
233 fn powder_snow_fall_sound_uses_vanilla_living_thresholds() {
234 init_vanilla_registry();
235 assert!(PowderSnowBlock::fall_sound(fall_context(3.99, true)).is_none());
236 assert_eq!(
237 PowderSnowBlock::fall_sound(fall_context(4.0, true)),
238 Some(&sound_events::ENTITY_PLAYER_SMALL_FALL)
239 );
240 assert_eq!(
241 PowderSnowBlock::fall_sound(fall_context(7.0, true)),
242 Some(&sound_events::ENTITY_PLAYER_BIG_FALL)
243 );
244 assert!(PowderSnowBlock::fall_sound(fall_context(7.0, false)).is_none());
245 }
246
247 #[test]
248 fn falling_entities_collide_with_lower_powder_snow_shape() {
249 init_vanilla_registry();
250 let behavior = powder_snow();
251 let state = powder_snow_state();
252 let pos = BlockPos::new(0, 64, 0);
253
254 let shape = behavior.get_collision_shape(
255 state,
256 &empty_level(),
257 pos,
258 BlockCollisionContext::entity(64.0, false)
259 .with_fall_distance(NUM_BLOCKS_TO_FALL_INTO_BLOCK + 0.01),
260 );
261
262 assert_eq!(shape, FALLING_COLLISION_SHAPE);
263 }
264
265 #[test]
266 fn walkable_entities_use_default_powder_snow_collision_shape_when_above() {
267 init_vanilla_registry();
268 let behavior = powder_snow();
269 let state = powder_snow_state();
270 let pos = BlockPos::new(0, 64, 0);
271 let context = BlockCollisionContext::entity(65.0, false).with_can_walk_on_powder_snow(true);
272
273 let shape = behavior.get_collision_shape(state, &empty_level(), pos, context);
274
275 assert_eq!(
276 shape,
277 behavior.default_get_collision_shape(state, &empty_level(), pos, context)
278 );
279 }
280
281 #[test]
282 fn non_walkable_or_descending_entities_have_no_powder_snow_collision() {
283 init_vanilla_registry();
284 let behavior = powder_snow();
285 let state = powder_snow_state();
286 let pos = BlockPos::new(0, 64, 0);
287
288 let non_walkable_shape = behavior.get_collision_shape(
289 state,
290 &empty_level(),
291 pos,
292 BlockCollisionContext::entity(65.0, false),
293 );
294 let descending_shape = behavior.get_collision_shape(
295 state,
296 &empty_level(),
297 pos,
298 BlockCollisionContext::entity(65.0, true).with_can_walk_on_powder_snow(true),
299 );
300
301 assert_eq!(non_walkable_shape, VoxelShape::EMPTY);
302 assert_eq!(descending_shape, VoxelShape::EMPTY);
303 }
304
305 #[test]
306 fn falling_blocks_use_default_powder_snow_collision_shape() {
307 init_vanilla_registry();
308 let behavior = powder_snow();
309 let state = powder_snow_state();
310 let pos = BlockPos::new(0, 64, 0);
311 let context = BlockCollisionContext::entity(64.0, true).with_falling_block(true);
312
313 let shape = behavior.get_collision_shape(state, &empty_level(), pos, context);
314
315 assert_eq!(
316 shape,
317 behavior.default_get_collision_shape(state, &empty_level(), pos, context)
318 );
319 }
320
321 #[test]
322 fn placement_context_has_no_powder_snow_collision() {
323 init_vanilla_registry();
324 let behavior = powder_snow();
325 let state = powder_snow_state();
326 let pos = BlockPos::new(0, 64, 0);
327
328 let shape = behavior.get_collision_shape(
329 state,
330 &empty_level(),
331 pos,
332 BlockCollisionContext::pre_move(65.0, false)
333 .with_can_walk_on_powder_snow(true)
334 .with_fall_distance(NUM_BLOCKS_TO_FALL_INTO_BLOCK + 0.01),
335 );
336
337 assert_eq!(shape, VoxelShape::EMPTY);
338 }
339}