1use 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#[block_behavior]
31pub struct FrostedIceBlock {
32 block: BlockRef,
33}
34
35impl FrostedIceBlock {
36 #[must_use]
38 pub const fn new(block: BlockRef) -> Self {
39 Self { block }
40 }
41
42 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 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}