Skip to main content

steel_core/behavior/blocks/vegetation/
pumpkin_block.rs

1use crate::behavior::block::drop_from_block_interact_loot_table;
2use crate::behavior::{
3    BlockBehavior, BlockPlaceContext, BlockRef, InteractionResult, InventoryAccess,
4};
5use crate::entity::Entity;
6use crate::player::Player;
7use crate::world::World;
8use crate::world::game_event::GameEventContext;
9use glam::DVec3;
10use rand::RngExt;
11use std::sync::Arc;
12use steel_macros::block_behavior;
13use steel_registry::blocks::block_state_ext::BlockStateExt;
14use steel_registry::blocks::properties::{BlockStateProperties, Direction, EnumProperty};
15use steel_registry::items::item::BlockHitResult;
16use steel_registry::stat::vanilla_stat_types;
17use steel_registry::{
18    sound_events, vanilla_blocks, vanilla_game_events, vanilla_items, vanilla_loot_tables,
19};
20use steel_utils::axis::Axis;
21use steel_utils::types::{InteractionHand, UpdateFlags};
22use steel_utils::{BlockPos, BlockStateId};
23
24/// Behavior for pumpkins.
25#[block_behavior]
26pub struct PumpkinBlock {
27    block: BlockRef,
28}
29
30const HORIZONTAL_FACING: &EnumProperty<Direction> = &BlockStateProperties::HORIZONTAL_FACING;
31
32impl PumpkinBlock {
33    /// Creates a pumpkin block behavior.
34    #[must_use]
35    pub const fn new(block: BlockRef) -> Self {
36        Self { block }
37    }
38}
39
40impl BlockBehavior for PumpkinBlock {
41    fn get_state_for_placement(&self, _context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
42        Some(self.block.default_state())
43    }
44
45    fn use_item_on(
46        &self,
47        state: BlockStateId,
48        world: &Arc<World>,
49        pos: BlockPos,
50        player: &Player,
51        _hand: InteractionHand,
52        hit_result: &BlockHitResult,
53        inv: &mut InventoryAccess,
54    ) -> InteractionResult {
55        let mut rng = rand::rng();
56
57        let Some(shears) = inv.with_item(|item_stack| {
58            item_stack
59                .is(&vanilla_items::SHEARS)
60                .then(|| item_stack.clone())
61        }) else {
62            return InteractionResult::TryEmptyHandInteraction;
63        };
64
65        let drops = drop_from_block_interact_loot_table(
66            &vanilla_loot_tables::CARVE_PUMPKIN,
67            state,
68            world.get_block_entity(pos),
69            Some(&shears),
70            Some(player),
71            &mut rng,
72        );
73
74        let clicked_direction = hit_result.direction;
75        let direction = if clicked_direction.axis() == Axis::Y {
76            player.direction_yaw().opposite()
77        } else {
78            clicked_direction
79        };
80
81        let (x_offset, z_offset) = {
82            let (x, _, z) = direction.offset();
83            (f64::from(x), f64::from(z))
84        };
85
86        for drop in drops {
87            world.spawn_item_with_velocity(
88                DVec3::new(
89                    f64::from(pos.x()) + 0.5 + x_offset * 0.65,
90                    f64::from(pos.y()) + 0.1,
91                    f64::from(pos.z()) + 0.5 + z_offset * 0.65,
92                ),
93                drop,
94                DVec3::new(
95                    rng.random_range(0.05 * x_offset..0.05 * x_offset + 0.02),
96                    0.05,
97                    rng.random_range(0.05 * z_offset..0.05 * z_offset + 0.02),
98                ),
99            );
100        }
101
102        world.play_block_sound(&sound_events::BLOCK_PUMPKIN_CARVE, pos, 1.0, 1.0, None);
103        world.set_block(
104            pos,
105            vanilla_blocks::CARVED_PUMPKIN
106                .default_state()
107                .set_value(HORIZONTAL_FACING, direction),
108            UpdateFlags::UPDATE_IMMEDIATE
109                | UpdateFlags::UPDATE_CLIENTS
110                | UpdateFlags::UPDATE_NEIGHBORS,
111        );
112        let has_infinite_materials = player.has_infinite_materials();
113        inv.with_item(|item_stack| item_stack.hurt_and_break(1, has_infinite_materials));
114
115        world.game_event(
116            &vanilla_game_events::SHEAR,
117            pos,
118            &GameEventContext::new(Some(player), None),
119        );
120
121        player.award_stat(&vanilla_stat_types::ITEM_USED, &vanilla_items::SHEARS);
122
123        InteractionResult::Success
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use std::sync::mpsc;
130    use std::thread;
131    use std::time::Duration;
132
133    use steel_registry::item_stack::ItemStack;
134    use steel_utils::{ChunkPos, Downcast as _};
135
136    use super::*;
137    use crate::bootstrap::init_globals;
138    use crate::entity::entities::ItemEntity;
139    use crate::test_support::{
140        DROPPED_ITEM_SEARCH_SIZE, TestPlayerBuilder, dropped_items, fresh_test_world,
141        insert_ready_full_chunk,
142    };
143
144    const CARVE_TIMEOUT: Duration = Duration::from_secs(5);
145
146    #[test]
147    fn carving_with_shears_drops_seeds_and_damages_shears() {
148        init_globals();
149        let world_fixture = fresh_test_world("pumpkin_carving");
150        let world = &world_fixture.world;
151        let pos = BlockPos::new(8, 64, 8);
152        let _holder = insert_ready_full_chunk(world, ChunkPos::from_block_pos(pos));
153        let state = vanilla_blocks::PUMPKIN.default_state();
154        assert!(world.set_block(pos, state, UpdateFlags::UPDATE_NONE));
155        let player =
156            TestPlayerBuilder::new(Arc::clone(world), "PumpkinCarver".to_owned(), 1).build();
157        player.inventory.lock().set_item_in_hand(
158            InteractionHand::MainHand,
159            ItemStack::new(&vanilla_items::SHEARS),
160        );
161
162        let (sender, receiver) = mpsc::channel();
163        {
164            let world = Arc::clone(world);
165            let player = Arc::clone(&player);
166            thread::spawn(move || {
167                let mut inv =
168                    InventoryAccess::new(Arc::clone(&player.inventory), InteractionHand::MainHand);
169                let hit_result = BlockHitResult {
170                    location: DVec3::new(8.5, 64.5, 8.0),
171                    direction: Direction::North,
172                    block_pos: pos,
173                    miss: false,
174                    inside: false,
175                    world_border_hit: false,
176                };
177                let result = PumpkinBlock::new(&vanilla_blocks::PUMPKIN).use_item_on(
178                    state,
179                    &world,
180                    pos,
181                    &player,
182                    InteractionHand::MainHand,
183                    &hit_result,
184                    &mut inv,
185                );
186                let _ = sender.send(result);
187            });
188        }
189        let Ok(result) = receiver.recv_timeout(CARVE_TIMEOUT) else {
190            panic!("carving a pumpkin should not deadlock on the player's inventory lock");
191        };
192
193        assert_eq!(result, InteractionResult::Success);
194        let carved = world.get_block_state(pos);
195        assert_eq!(carved.get_block(), &vanilla_blocks::CARVED_PUMPKIN);
196        assert_eq!(carved.get_value(HORIZONTAL_FACING), Direction::North);
197        assert_eq!(
198            player
199                .inventory
200                .lock()
201                .get_item_in_hand(InteractionHand::MainHand)
202                .get_damage_value(),
203            1
204        );
205
206        let dropped = dropped_items(
207            world,
208            DVec3::new(8.5, 64.0 + DROPPED_ITEM_SEARCH_SIZE / 2.0, 8.5),
209        );
210        assert_eq!(dropped.len(), 1);
211        let Some(seeds) = dropped[0].downcast_ref::<ItemEntity>() else {
212            panic!("carved pumpkin drop should be an item entity");
213        };
214        let seeds = seeds.get_item();
215        assert!(seeds.is(&vanilla_items::PUMPKIN_SEEDS));
216        assert_eq!(seeds.count, 4);
217    }
218}