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