steel_core/behavior/blocks/vegetation/
turtle_egg_block.rs1use std::sync::Arc;
8
9use steel_macros::block_behavior;
10use steel_registry::blocks::BlockRef;
11use steel_registry::blocks::block_state_ext::BlockStateExt;
12use steel_registry::blocks::properties::{BlockStateProperties, IntProperty};
13use steel_registry::item_stack::ItemStack;
14use steel_registry::vanilla_block_tags::BlockTag;
15use steel_registry::{
16 level_events, sound_events, vanilla_entities, vanilla_game_events, vanilla_game_rules,
17 vanilla_items, vanilla_world_clocks,
18};
19use steel_utils::types::UpdateFlags;
20use steel_utils::{BlockPos, BlockStateId};
21
22use crate::behavior::block::{
23 BlockBehavior, EntityFallDamage, EntityFallOnContext, default_can_be_replaced,
24};
25use crate::behavior::context::BlockPlaceContext;
26use crate::block_entity::SharedBlockEntity;
27use crate::entity::Entity;
28use crate::player::Player;
29use crate::world::World;
30use crate::world::game_event::GameEventContext;
31
32const MAX_HATCH_LEVEL: u8 = 2;
34const MAX_EGGS: u8 = 4;
36
37const TICKS_PER_DAY: i64 = 24_000;
39const BASE_HATCH_CHANCE: f32 = 0.002;
41const HATCH_WINDOW_START: i64 = 21_062;
43const HATCH_WINDOW_END: i64 = 21_905;
45
46const STEP_TRAMPLE_ODDS: i32 = 100;
48const FALL_TRAMPLE_ODDS: i32 = 3;
50
51const TURTLE_EGG_PLACEMENT_PARTICLE_COUNT: i32 = 15;
53
54const OVERWORLD_TIMELINE_TAG: &str = "#minecraft:in_overworld";
59
60const HATCH: &IntProperty = &BlockStateProperties::HATCH;
61const EGGS: &IntProperty = &BlockStateProperties::EGGS;
62
63#[block_behavior]
65pub struct TurtleEggBlock {
66 block: BlockRef,
67}
68
69impl TurtleEggBlock {
70 #[must_use]
72 pub const fn new(block: BlockRef) -> Self {
73 Self { block }
74 }
75
76 fn on_sand(world: &Arc<World>, pos: BlockPos) -> bool {
79 world
80 .get_block_state(pos.below())
81 .get_block()
82 .has_tag(&BlockTag::SAND)
83 }
84
85 fn should_update_hatch_level(world: &Arc<World>) -> bool {
102 let day_time = world
103 .clock_total_ticks(&vanilla_world_clocks::OVERWORLD)
104 .unwrap_or(0)
105 .rem_euclid(TICKS_PER_DAY);
106
107 let in_hatch_window = (HATCH_WINDOW_START..HATCH_WINDOW_END).contains(&day_time)
108 && world.dimension_type.timelines == Some(OVERWORLD_TIMELINE_TAG);
109 let chance = if in_hatch_window {
110 1.0
111 } else {
112 BASE_HATCH_CHANCE
113 };
114
115 chance > 0.0 && rand::random::<f32>() < chance
116 }
117
118 fn can_destroy_egg(world: &Arc<World>, entity: &dyn Entity) -> bool {
122 let entity_type = entity.entity_type();
123 if entity_type == &vanilla_entities::TURTLE || entity_type == &vanilla_entities::BAT {
124 return false;
125 }
126 if !entity.is_living_entity() {
127 return false;
128 }
129 entity_type == &vanilla_entities::PLAYER
130 || world.get_game_rule(&vanilla_game_rules::MOB_GRIEFING)
131 }
132
133 fn destroy_egg(
136 &self,
137 world: &Arc<World>,
138 state: BlockStateId,
139 pos: BlockPos,
140 entity: &dyn Entity,
141 odds: i32,
142 ) {
143 if state.get_block() == self.block
144 && Self::can_destroy_egg(world, entity)
145 && rand::random_range(0..odds) == 0
146 {
147 Self::decrease_eggs(world, pos, state);
148 }
149 }
150
151 fn decrease_eggs(world: &Arc<World>, pos: BlockPos, state: BlockStateId) {
154 world.play_block_sound(
155 &sound_events::ENTITY_TURTLE_EGG_BREAK,
156 pos,
157 0.7,
158 rand::random_range(0.9..1.1),
159 None,
160 );
161
162 let eggs = state.get_value(EGGS);
163 if eggs <= 1 {
164 world.destroy_block(pos, false);
165 } else {
166 world.set_block(
167 pos,
168 state.set_value(EGGS, eggs - 1),
169 UpdateFlags::UPDATE_CLIENTS,
170 );
171 world.game_event(
172 &vanilla_game_events::BLOCK_DESTROY,
173 pos,
174 &GameEventContext::new(None, Some(state)),
175 );
176 world.level_event(
177 level_events::PARTICLES_DESTROY_BLOCK,
178 pos,
179 level_events::encode_block_state_data(u32::from(state.0)),
180 None,
181 );
182 }
183 }
184}
185
186impl BlockBehavior for TurtleEggBlock {
187 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
188 let existing = context.world.get_block_state(context.place_pos());
189 if existing.get_block() == self.block {
190 Some(existing.set_value(EGGS, (existing.get_value(EGGS) + 1).min(MAX_EGGS)))
192 } else {
193 Some(self.block.default_state())
194 }
195 }
196
197 fn can_be_replaced(&self, state: BlockStateId, context: &BlockPlaceContext<'_>) -> bool {
198 if !context.is_secondary_use_active()
199 && context.with_item(|item| item.is(&vanilla_items::TURTLE_EGG))
200 && state.get_value(EGGS) < MAX_EGGS
201 {
202 true
203 } else {
204 default_can_be_replaced(state, context)
205 }
206 }
207
208 fn on_place(
209 &self,
210 _state: BlockStateId,
211 world: &Arc<World>,
212 pos: BlockPos,
213 _old_state: BlockStateId,
214 _moved_by_piston: bool,
215 ) {
216 if Self::on_sand(world, pos) {
218 world.level_event(
219 level_events::PARTICLES_TURTLE_EGG_PLACEMENT,
220 pos,
221 TURTLE_EGG_PLACEMENT_PARTICLE_COUNT,
222 None,
223 );
224 }
225 }
226
227 fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
228 if !Self::should_update_hatch_level(world) || !Self::on_sand(world, pos) {
229 return;
230 }
231
232 let hatch = state.get_value(HATCH);
233 if hatch < MAX_HATCH_LEVEL {
234 world.play_block_sound(
235 &sound_events::ENTITY_TURTLE_EGG_CRACK,
236 pos,
237 0.7,
238 rand::random_range(0.9..1.1),
239 None,
240 );
241 world.set_block(
242 pos,
243 state.set_value(HATCH, hatch + 1),
244 UpdateFlags::UPDATE_CLIENTS,
245 );
246 world.game_event(
247 &vanilla_game_events::BLOCK_CHANGE,
248 pos,
249 &GameEventContext::new(None, Some(state)),
250 );
251 } else {
252 world.play_block_sound(
253 &sound_events::ENTITY_TURTLE_EGG_HATCH,
254 pos,
255 0.7,
256 rand::random_range(0.9..1.1),
257 None,
258 );
259 world.remove_block(pos, false);
260 world.game_event(
261 &vanilla_game_events::BLOCK_DESTROY,
262 pos,
263 &GameEventContext::new(None, Some(state)),
264 );
265
266 let eggs = state.get_value(EGGS);
267 for _ in 0..eggs {
268 world.level_event(
269 level_events::PARTICLES_DESTROY_BLOCK,
270 pos,
271 level_events::encode_block_state_data(u32::from(state.0)),
272 None,
273 );
274 }
279 }
280 }
281
282 fn step_on(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos, entity: &dyn Entity) {
283 if !entity.is_stepping_carefully() {
285 self.destroy_egg(world, state, pos, entity, STEP_TRAMPLE_ODDS);
286 }
287 self.default_step_on(state, world, pos, entity);
288 }
289
290 fn fall_on(
291 &self,
292 state: BlockStateId,
293 world: &Arc<World>,
294 pos: BlockPos,
295 context: EntityFallOnContext<'_>,
296 ) -> Option<EntityFallDamage> {
297 if let Some(entity) = context.source_entity()
298 && entity.entity_type() != &vanilla_entities::ZOMBIE
299 {
300 self.destroy_egg(world, state, pos, entity, FALL_TRAMPLE_ODDS);
307 }
308 self.default_fall_on(state, world, pos, context)
309 }
310
311 fn player_destroy(
312 &self,
313 world: &Arc<World>,
314 _player: &Player,
315 pos: BlockPos,
316 state: BlockStateId,
317 _block_entity: Option<&SharedBlockEntity>,
318 _tool: &ItemStack,
319 ) {
320 Self::decrease_eggs(world, pos, state);
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use steel_registry::{init_vanilla_registry, vanilla_blocks, vanilla_world_clocks};
332 use steel_utils::ChunkPos;
333
334 use super::*;
335 use crate::behavior::{BLOCK_BEHAVIORS, init_behaviors};
336 use crate::test_support::{fresh_test_world, insert_ready_full_chunk};
337
338 const ALWAYS_HATCH_DAY_TIME: i64 = 21_500;
341
342 fn prepare(key: &'static str) -> (Arc<World>, BlockPos) {
343 init_vanilla_registry();
344 init_behaviors();
345 let world = fresh_test_world(key);
346 let pos = BlockPos::new(8, 64, 8);
347 insert_ready_full_chunk(&world, ChunkPos::from_block_pos(pos));
348 world.set_clock_total_ticks(&vanilla_world_clocks::OVERWORLD, ALWAYS_HATCH_DAY_TIME);
349 (world, pos)
350 }
351
352 #[test]
353 fn eggs_crack_twice_then_hatch_on_sand() {
354 let (world, pos) = prepare("turtle_egg_hatch");
355 assert!(world.set_block(
356 pos.below(),
357 vanilla_blocks::SAND.default_state(),
358 UpdateFlags::UPDATE_NONE,
359 ));
360 assert!(world.set_block(
361 pos,
362 vanilla_blocks::TURTLE_EGG.default_state(),
363 UpdateFlags::UPDATE_NONE,
364 ));
365 let behavior = BLOCK_BEHAVIORS.get_behavior(&vanilla_blocks::TURTLE_EGG);
366
367 behavior.random_tick(world.get_block_state(pos), &world, pos);
368 assert_eq!(world.get_block_state(pos).get_value(HATCH), 1);
369
370 behavior.random_tick(world.get_block_state(pos), &world, pos);
371 assert_eq!(world.get_block_state(pos).get_value(HATCH), 2);
372
373 behavior.random_tick(world.get_block_state(pos), &world, pos);
377 assert!(world.get_block_state(pos).is_air());
378 }
379
380 #[test]
381 fn eggs_do_not_advance_off_sand() {
382 let (world, pos) = prepare("turtle_egg_off_sand");
383 assert!(world.set_block(
384 pos.below(),
385 vanilla_blocks::STONE.default_state(),
386 UpdateFlags::UPDATE_NONE,
387 ));
388 assert!(world.set_block(
389 pos,
390 vanilla_blocks::TURTLE_EGG.default_state(),
391 UpdateFlags::UPDATE_NONE,
392 ));
393 let behavior = BLOCK_BEHAVIORS.get_behavior(&vanilla_blocks::TURTLE_EGG);
394
395 behavior.random_tick(world.get_block_state(pos), &world, pos);
396 assert_eq!(world.get_block_state(pos).get_value(HATCH), 0);
397 }
398}