From 788e369786681c30565c19307f575d6b1a49d3b8 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Wed, 12 Aug 2026 21:03:00 +0200 Subject: [PATCH] feat(block): implement turtle egg block --- crates/pumpkin/src/block/blocks/mod.rs | 1 + .../pumpkin/src/block/blocks/sniffer_egg.rs | 8 +- crates/pumpkin/src/block/blocks/turtle_egg.rs | 151 ++++++++++++++++++ crates/pumpkin/src/block/registry.rs | 7 +- 4 files changed, 162 insertions(+), 5 deletions(-) create mode 100644 crates/pumpkin/src/block/blocks/turtle_egg.rs diff --git a/crates/pumpkin/src/block/blocks/mod.rs b/crates/pumpkin/src/block/blocks/mod.rs index ac644782c..b19767029 100644 --- a/crates/pumpkin/src/block/blocks/mod.rs +++ b/crates/pumpkin/src/block/blocks/mod.rs @@ -115,4 +115,5 @@ pub mod creaking_heart; pub mod decorated_pot; pub mod sniffer_egg; pub mod trial_spawner; +pub mod turtle_egg; pub mod vault; diff --git a/crates/pumpkin/src/block/blocks/sniffer_egg.rs b/crates/pumpkin/src/block/blocks/sniffer_egg.rs index f9acca912..34653c9ff 100644 --- a/crates/pumpkin/src/block/blocks/sniffer_egg.rs +++ b/crates/pumpkin/src/block/blocks/sniffer_egg.rs @@ -76,8 +76,12 @@ impl BlockBehaviour for SnifferEggBlock { let on_moss = Self::is_on_moss(args.world.as_ref(), args.position); let delay = Self::get_hatch_delay(on_moss); - args.world - .schedule_block_tick(args.block, *args.position, delay, TickPriority::Normal); + args.world.schedule_block_tick( + args.block, + *args.position, + delay, + TickPriority::Normal, + ); } else { args.world .break_block(args.position, None, BlockFlags::SKIP_DROPS) diff --git a/crates/pumpkin/src/block/blocks/turtle_egg.rs b/crates/pumpkin/src/block/blocks/turtle_egg.rs new file mode 100644 index 000000000..cdcc87bd9 --- /dev/null +++ b/crates/pumpkin/src/block/blocks/turtle_egg.rs @@ -0,0 +1,151 @@ +use pumpkin_data::block_properties::{BlockProperties, TurtleEggLikeProperties}; +use pumpkin_data::entity::EntityPose; +use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_data::tag::Taggable; +use pumpkin_data::{BlockDirection, BlockStateId, tag}; +use pumpkin_macros::pumpkin_block; +use pumpkin_util::math::position::BlockPos; +use pumpkin_world::tick::TickPriority; +use pumpkin_world::world::{BlockAccessor, BlockFlags}; + +use crate::block::{ + BlockBehaviour, BlockFuture, BlockIsReplacing, BrokenArgs, CanPlaceAtArgs, CanUpdateAtArgs, + GetStateForNeighborUpdateArgs, OnLandedUponArgs, OnPlaceArgs, OnScheduledTickArgs, + RandomTickArgs, +}; +use crate::entity::EntityBase; + +type TurtleEggProperties = TurtleEggLikeProperties; + +#[pumpkin_block("minecraft:turtle_egg")] +pub struct TurtleEggBlock; + +impl BlockBehaviour for TurtleEggBlock { + fn on_place<'a>(&'a self, args: OnPlaceArgs<'a>) -> BlockFuture<'a, BlockStateId> { + Box::pin(async move { + if args.player.get_entity().pose.load() != EntityPose::Crouching + && let BlockIsReplacing::Itself(state_id) = args.replacing + { + let mut properties = TurtleEggProperties::from_state_id(state_id, args.block); + if properties.eggs < 4 { + properties.eggs += 1; + } + return properties.to_state_id(args.block); + } + + let properties = TurtleEggProperties::default(args.block); + properties.to_state_id(args.block) + }) + } + + fn can_place_at(&self, args: CanPlaceAtArgs<'_>) -> bool { + can_place_at(args.block_accessor, args.position) + } + + fn can_update_at(&self, args: CanUpdateAtArgs<'_>) -> bool { + let b = BlockAccessor::get_block(args.world, args.position); + args.player.get_entity().pose.load() != EntityPose::Crouching + && TurtleEggProperties::from_state_id(args.state_id, args.block).eggs < 4 + && args.block.id == b.id + } + + fn get_state_for_neighbor_update<'a>( + &'a self, + args: GetStateForNeighborUpdateArgs<'a>, + ) -> BlockFuture<'a, BlockStateId> { + Box::pin(async move { + if !can_place_at(args.world, args.position) { + args.world + .schedule_block_tick(args.block, *args.position, 1, TickPriority::Normal); + } + args.state_id + }) + } + + fn on_scheduled_tick<'a>(&'a self, args: OnScheduledTickArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + if !can_place_at(args.world.as_ref(), args.position) { + args.world + .break_block(args.position, None, BlockFlags::empty()) + .await; + } + }) + } + + fn random_tick<'a>(&'a self, args: RandomTickArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + // Turtle eggs can only hatch when placed on sand + if !args + .world + .get_block(&args.position.down()) + .has_tag(&tag::Block::MINECRAFT_SAND) + { + return; + } + + let state_id = args.world.get_block_state_id(args.position); + let mut props = TurtleEggProperties::from_state_id(state_id, args.block); + + if props.hatch < 2 { + props.hatch += 1; + args.world + .set_block_state( + args.position, + props.to_state_id(args.block), + BlockFlags::NOTIFY_ALL, + ) + .await; + + args.world.play_sound( + Sound::EntityTurtleEggCrack, + SoundCategory::Blocks, + &args.position.to_f64(), + ); + } else { + args.world + .break_block(args.position, None, BlockFlags::SKIP_DROPS) + .await; + + args.world.play_sound( + Sound::EntityTurtleEggHatch, + SoundCategory::Blocks, + &args.position.to_f64(), + ); + } + }) + } + + fn on_landed_upon<'a>(&'a self, args: OnLandedUponArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + if let Some(living) = args.entity.get_living_entity() { + living + .handle_fall_damage(args.entity, args.fall_distance, 1.0) + .await; + } + + // Entity falling onto turtle egg breaks an egg + if args.fall_distance > 0.5 { + args.world.play_sound( + Sound::EntityTurtleEggBreak, + SoundCategory::Blocks, + &args.entity.get_entity().pos.load(), + ); + } + }) + } + + fn broken<'a>(&'a self, args: BrokenArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + args.world.play_sound( + Sound::EntityTurtleEggBreak, + SoundCategory::Blocks, + &args.position.to_f64(), + ); + }) + } +} + +fn can_place_at(block_accessor: &dyn BlockAccessor, position: &BlockPos) -> bool { + let (support_block, state) = block_accessor.get_block_and_state(&position.down()); + support_block.has_tag(&tag::Block::MINECRAFT_SAND) || state.is_center_solid(BlockDirection::Up) +} diff --git a/crates/pumpkin/src/block/registry.rs b/crates/pumpkin/src/block/registry.rs index bfad5cb22..9f41da9ab 100644 --- a/crates/pumpkin/src/block/registry.rs +++ b/crates/pumpkin/src/block/registry.rs @@ -7,6 +7,7 @@ use crate::block::blocks::beacon::BeaconBlock; use crate::block::blocks::bed::BedBlock; use crate::block::blocks::beehive::BeehiveBlock; use crate::block::blocks::brewing_stand::BrewingStandBlock; +use crate::block::blocks::brushable_block::BrushableBlock; use crate::block::blocks::bubble_column::BubbleColumnBlock; use crate::block::blocks::cake::CakeBlock; use crate::block::blocks::campfire::CampfireBlock; @@ -127,10 +128,8 @@ use crate::block::blocks::sculk::sculk_vein::SculkVeinBlock; use crate::block::blocks::shelf::ShelfBlock; use crate::block::blocks::signs::SignBlock; use crate::block::blocks::slabs::SlabBlock; -use crate::block::blocks::slime::SlimeBlock; -use crate::block::blocks::snow::LayeredSnowBlock; -use crate::block::blocks::brushable_block::BrushableBlock; use crate::block::blocks::sniffer_egg::SnifferEggBlock; +use crate::block::blocks::snow::LayeredSnowBlock; use crate::block::blocks::spawner::SpawnerBlock; use crate::block::blocks::sponge::{SpongeBlock, WetSpongeBlock}; use crate::block::blocks::stairs::StairBlock; @@ -138,6 +137,7 @@ use crate::block::blocks::tnt::TNTBlock; use crate::block::blocks::torches::TorchBlock; use crate::block::blocks::trapdoor::TrapDoorBlock; use crate::block::blocks::trial_spawner::TrialSpawnerBlock; +use crate::block::blocks::turtle_egg::TurtleEggBlock; use crate::block::blocks::vault::VaultBlock; use crate::block::blocks::vine::VineBlock; use crate::block::blocks::walls::WallBlock; @@ -259,6 +259,7 @@ pub fn default_registry() -> Arc { manager.register(DecoratedPotBlock); manager.register(CreakingHeartBlock); manager.register(SnifferEggBlock); + manager.register(TurtleEggBlock); manager.register(BrushableBlock); manager.register(BushBlock); manager.register(FlowerBlock);