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