Skip to main content

steel_core/behavior/blocks/building/
frosted_ice_block.rs

1//! `FrostedIceBlock` behavior (`net.minecraft.world.level.block.FrostedIceBlock`).
2
3use std::sync::Arc;
4
5use steel_macros::block_behavior;
6use steel_registry::blocks::BlockRef;
7use steel_registry::blocks::block_state_ext::BlockStateExt;
8use steel_registry::blocks::properties::{BlockStateProperties, IntProperty};
9use steel_registry::item_stack::ItemStack;
10use steel_utils::types::UpdateFlags;
11use steel_utils::{BlockPos, BlockStateId, Direction};
12
13use super::ice_block::{BASE_MELT_LIGHT_LEVEL, IceBlock};
14use crate::behavior::{BlockBehavior, BlockPlaceContext};
15use crate::block_entity::SharedBlockEntity;
16use crate::chunk::light::LightLayer;
17use crate::player::Player;
18use crate::world::{LevelReader, World};
19
20const AGE: &IntProperty = &BlockStateProperties::AGE_3;
21const MAX_AGE: u8 = 3;
22const NEIGHBORS_TO_AGE: u8 = 4;
23const NEIGHBORS_TO_MELT: u8 = 2;
24const PLACE_TICK_MIN: i32 = 60;
25const PLACE_TICK_MAX: i32 = 120;
26const MELT_TICK_MIN: i32 = 20;
27const MELT_TICK_MAX: i32 = 40;
28
29/// Vanilla `FrostedIceBlock`.
30#[block_behavior]
31pub struct FrostedIceBlock {
32    block: BlockRef,
33}
34
35impl FrostedIceBlock {
36    /// Creates a frosted ice block behavior.
37    #[must_use]
38    pub const fn new(block: BlockRef) -> Self {
39        Self { block }
40    }
41
42    /// Vanilla `FrostedIceBlock` neighbor-count check.
43    fn fewer_neighbors_than(&self, world: &dyn LevelReader, pos: BlockPos, limit: u8) -> bool {
44        let mut count = 0;
45        for direction in Direction::ALL {
46            if world.get_block_state(pos.relative(direction)).get_block() == self.block {
47                count += 1;
48                if count >= limit {
49                    return false;
50                }
51            }
52        }
53        true
54    }
55
56    /// Vanilla `FrostedIceBlock.slightlyMelt`.
57    ///
58    /// Returns `true` when the block fully melted into water or air.
59    fn slightly_melt(state: BlockStateId, world: &Arc<World>, pos: BlockPos) -> bool {
60        let age = state.get_value(AGE);
61        if age < MAX_AGE {
62            world.set_block(
63                pos,
64                state.set_value(AGE, age + 1),
65                UpdateFlags::UPDATE_CLIENTS,
66            );
67            false
68        } else {
69            IceBlock::melt(state, world, pos);
70            true
71        }
72    }
73
74    fn melt_tick_delay() -> i32 {
75        rand::random_range(MELT_TICK_MIN..=MELT_TICK_MAX)
76    }
77
78    fn melt_brightness(world: &Arc<World>, pos: BlockPos, state: BlockStateId) -> bool {
79        let brightness = if world.is_end_dimension_type() {
80            world.light_value_at(LightLayer::Block, pos)
81        } else {
82            world.max_local_raw_brightness(pos, world.sky_darkening())
83        };
84        i32::from(brightness)
85            > i32::from(BASE_MELT_LIGHT_LEVEL)
86                - i32::from(state.get_value(AGE))
87                - i32::from(state.get_light_dampening())
88    }
89}
90
91impl BlockBehavior for FrostedIceBlock {
92    fn get_state_for_placement(&self, _context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
93        Some(self.block.default_state())
94    }
95
96    fn on_place(
97        &self,
98        _state: BlockStateId,
99        world: &Arc<World>,
100        pos: BlockPos,
101        _old_state: BlockStateId,
102        _moved_by_piston: bool,
103    ) {
104        world.schedule_block_tick_default(
105            pos,
106            self.block,
107            rand::random_range(PLACE_TICK_MIN..=PLACE_TICK_MAX),
108        );
109    }
110
111    #[expect(
112        clippy::collapsible_if,
113        reason = "matches vanilla FrostedIceBlock.tick control flow"
114    )]
115    fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
116        if rand::random_range(0..3) == 0
117            || self.fewer_neighbors_than(world.as_ref(), pos, NEIGHBORS_TO_AGE)
118        {
119            if Self::melt_brightness(world, pos, state) && Self::slightly_melt(state, world, pos) {
120                for direction in Direction::ALL {
121                    let neighbor_pos = pos.relative(direction);
122                    let neighbor = world.get_block_state(neighbor_pos);
123                    if neighbor.get_block() == self.block
124                        && !Self::slightly_melt(neighbor, world, neighbor_pos)
125                    {
126                        world.schedule_block_tick_default(
127                            neighbor_pos,
128                            self.block,
129                            Self::melt_tick_delay(),
130                        );
131                    }
132                }
133                return;
134            }
135        }
136
137        world.schedule_block_tick_default(pos, self.block, Self::melt_tick_delay());
138    }
139
140    fn handle_neighbor_changed(
141        &self,
142        state: BlockStateId,
143        world: &Arc<World>,
144        pos: BlockPos,
145        source_block: BlockRef,
146        _moved_by_piston: bool,
147    ) {
148        if source_block == self.block
149            && self.fewer_neighbors_than(world.as_ref(), pos, NEIGHBORS_TO_MELT)
150        {
151            IceBlock::melt(state, world, pos);
152        }
153    }
154
155    fn get_clone_item_stack(
156        &self,
157        _block: BlockRef,
158        _state: BlockStateId,
159        _include_data: bool,
160    ) -> Option<ItemStack> {
161        None
162    }
163
164    fn player_destroy(
165        &self,
166        world: &Arc<World>,
167        player: &Player,
168        pos: BlockPos,
169        state: BlockStateId,
170        block_entity: Option<&SharedBlockEntity>,
171        tool: &ItemStack,
172    ) {
173        IceBlock::new(self.block).player_destroy(world, player, pos, state, block_entity, tool);
174    }
175
176    fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
177        IceBlock::new(self.block).random_tick(state, world, pos);
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use steel_registry::blocks::block_state_ext::BlockStateExt;
184    use steel_registry::blocks::properties::BlockStateProperties;
185    use steel_registry::{init_vanilla_registry, vanilla_blocks, vanilla_dimension_types};
186    use steel_utils::{BlockPos, ChunkPos, Direction, Identifier, types::UpdateFlags};
187
188    use super::*;
189    use crate::behavior::init_behaviors;
190    use crate::test_support::{
191        TestLevel, fresh_test_world, fresh_test_world_with_dimension_type, insert_ready_full_chunk,
192    };
193
194    fn behavior() -> FrostedIceBlock {
195        FrostedIceBlock::new(&vanilla_blocks::FROSTED_ICE)
196    }
197
198    fn aged(age: u8) -> BlockStateId {
199        vanilla_blocks::FROSTED_ICE
200            .default_state()
201            .set_value(&BlockStateProperties::AGE_3, age)
202    }
203
204    fn world_with_block(key: &'static str, pos: BlockPos, state: BlockStateId) -> Arc<World> {
205        init_vanilla_registry();
206        init_behaviors();
207        let world = fresh_test_world(key);
208        insert_ready_full_chunk(&world, ChunkPos::from_block_pos(pos));
209        assert!(world.set_block(pos, state, UpdateFlags::UPDATE_NONE));
210        world
211    }
212
213    #[test]
214    fn pick_block_returns_empty() {
215        init_vanilla_registry();
216        let state = vanilla_blocks::FROSTED_ICE.default_state();
217        assert!(
218            behavior()
219                .get_clone_item_stack(&vanilla_blocks::FROSTED_ICE, state, false)
220                .is_none()
221        );
222    }
223
224    #[test]
225    fn custom_world_key_retains_end_dimension_semantics() {
226        let world = fresh_test_world_with_dimension_type(
227            "other",
228            "the_end",
229            &vanilla_dimension_types::THE_END,
230        );
231
232        assert_ne!(world.key, Identifier::vanilla_static("the_end"));
233        assert!(world.is_end_dimension_type());
234    }
235
236    #[test]
237    fn fewer_neighbors_than_counts_adjacent_frosted_ice() {
238        init_vanilla_registry();
239        let pos = BlockPos::ZERO;
240        let frosted = vanilla_blocks::FROSTED_ICE.default_state();
241        let isolated = TestLevel::default().with_block(pos, frosted);
242        assert!(behavior().fewer_neighbors_than(&isolated, pos, NEIGHBORS_TO_AGE));
243        assert!(behavior().fewer_neighbors_than(&isolated, pos, NEIGHBORS_TO_MELT));
244
245        let mut packed = TestLevel::default().with_block(pos, frosted);
246        for direction in Direction::ALL {
247            packed = packed.with_block(pos.relative(direction), frosted);
248        }
249        assert!(!behavior().fewer_neighbors_than(&packed, pos, NEIGHBORS_TO_AGE));
250        assert!(!behavior().fewer_neighbors_than(&packed, pos, NEIGHBORS_TO_MELT));
251
252        let one_neighbor = TestLevel::default()
253            .with_block(pos, frosted)
254            .with_block(pos.above(), frosted);
255        assert!(behavior().fewer_neighbors_than(&one_neighbor, pos, NEIGHBORS_TO_AGE));
256        assert!(behavior().fewer_neighbors_than(&one_neighbor, pos, NEIGHBORS_TO_MELT));
257    }
258
259    #[test]
260    fn slightly_melt_increments_age_before_melting() {
261        init_vanilla_registry();
262        let pos = BlockPos::new(8, 64, 8);
263        let world = world_with_block("frosted_ice_age", pos, aged(0));
264
265        assert!(!FrostedIceBlock::slightly_melt(
266            world.get_block_state(pos),
267            &world,
268            pos
269        ));
270        assert_eq!(world.get_block_state(pos).get_value(AGE), 1);
271
272        assert!(!FrostedIceBlock::slightly_melt(
273            world.get_block_state(pos),
274            &world,
275            pos
276        ));
277        assert_eq!(world.get_block_state(pos).get_value(AGE), 2);
278
279        assert!(!FrostedIceBlock::slightly_melt(
280            world.get_block_state(pos),
281            &world,
282            pos
283        ));
284        assert_eq!(world.get_block_state(pos).get_value(AGE), 3);
285
286        assert!(FrostedIceBlock::slightly_melt(
287            world.get_block_state(pos),
288            &world,
289            pos
290        ));
291        assert_eq!(
292            world.get_block_state(pos),
293            vanilla_blocks::WATER.default_state()
294        );
295    }
296
297    #[test]
298    fn on_place_schedules_a_tick() {
299        init_vanilla_registry();
300        let pos = BlockPos::new(8, 64, 8);
301        let world = world_with_block(
302            "frosted_ice_place",
303            pos,
304            vanilla_blocks::STONE.default_state(),
305        );
306        let state = vanilla_blocks::FROSTED_ICE.default_state();
307        assert!(world.set_block(
308            pos,
309            vanilla_blocks::AIR.default_state(),
310            UpdateFlags::UPDATE_NONE
311        ));
312        behavior().on_place(
313            state,
314            &world,
315            pos,
316            vanilla_blocks::AIR.default_state(),
317            false,
318        );
319        assert!(world.has_scheduled_block_tick(pos, &vanilla_blocks::FROSTED_ICE));
320    }
321
322    #[test]
323    fn neighbor_change_melts_isolated_frosted_ice() {
324        init_vanilla_registry();
325        let pos = BlockPos::new(8, 64, 8);
326        let world = world_with_block("frosted_ice_neighbor", pos, aged(0));
327
328        behavior().handle_neighbor_changed(
329            world.get_block_state(pos),
330            &world,
331            pos,
332            &vanilla_blocks::FROSTED_ICE,
333            false,
334        );
335        assert_eq!(
336            world.get_block_state(pos),
337            vanilla_blocks::WATER.default_state()
338        );
339    }
340
341    #[test]
342    fn neighbor_change_keeps_well_supported_frosted_ice() {
343        init_vanilla_registry();
344        let pos = BlockPos::new(8, 64, 8);
345        let world = world_with_block("frosted_ice_supported", pos, aged(0));
346        assert!(world.set_block(pos.above(), aged(0), UpdateFlags::UPDATE_NONE,));
347        assert!(world.set_block(pos.below(), aged(0), UpdateFlags::UPDATE_NONE,));
348
349        behavior().handle_neighbor_changed(
350            world.get_block_state(pos),
351            &world,
352            pos,
353            &vanilla_blocks::FROSTED_ICE,
354            false,
355        );
356        assert_eq!(
357            world.get_block_state(pos).get_block(),
358            &vanilla_blocks::FROSTED_ICE
359        );
360    }
361}