steel_core/behavior/blocks/vegetation/
pumpkin_block.rs1use 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#[block_behavior]
26pub struct PumpkinBlock {
27 block: BlockRef,
28}
29
30const HORIZONTAL_FACING: &EnumProperty<Direction> = &BlockStateProperties::HORIZONTAL_FACING;
31
32impl PumpkinBlock {
33 #[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}