steel_core/behavior/blocks/decoration/
flower_pot_block.rs1use crate::player::player_inventory::Prediction;
8use std::sync::{Arc, LazyLock};
9
10use rustc_hash::FxHashMap;
11use steel_macros::block_behavior;
12use steel_registry::blocks::BlockRef;
13use steel_registry::item_stack::ItemStack;
14use steel_registry::items::item::BlockHitResult;
15use steel_registry::vanilla_custom_stats;
16use steel_registry::{REGISTRY, vanilla_blocks, vanilla_game_events};
17use steel_utils::locks::SyncMutex;
18use steel_utils::types::{InteractionHand, UpdateFlags};
19use steel_utils::{BlockPos, BlockStateId, Direction, Identifier};
20
21use crate::behavior::{BlockBehavior, BlockPlaceContext, InteractionResult, InventoryAccess};
22use crate::block_entity::SharedBlockEntity;
23use crate::entity::ai::path::PathComputationType;
24use crate::player::Player;
25use crate::world::game_event::GameEventContext;
26use crate::world::{ScheduledTickAccess, World};
27
28static POTTED_BY_CONTENT: LazyLock<SyncMutex<FxHashMap<Identifier, BlockRef>>> =
32 LazyLock::new(|| SyncMutex::new(FxHashMap::default()));
33
34#[block_behavior]
36pub struct FlowerPotBlock {
37 block: BlockRef,
38 #[json_arg(vanilla_blocks, json = "potted")]
40 potted: BlockRef,
41}
42
43impl FlowerPotBlock {
44 #[must_use]
46 pub fn new(block: BlockRef, potted: BlockRef) -> Self {
47 if potted != &vanilla_blocks::AIR {
48 POTTED_BY_CONTENT.lock().insert(potted.key.clone(), block);
49 }
50 Self { block, potted }
51 }
52
53 fn is_empty(&self) -> bool {
54 self.potted == &vanilla_blocks::AIR
55 }
56
57 fn potted_block_for(content: BlockRef) -> Option<BlockRef> {
59 POTTED_BY_CONTENT.lock().get(&content.key).copied()
60 }
61}
62
63impl BlockBehavior for FlowerPotBlock {
64 fn get_state_for_placement(&self, _context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
65 Some(self.block.default_state())
66 }
67
68 fn use_item_on(
69 &self,
70 _state: BlockStateId,
71 world: &Arc<World>,
72 pos: BlockPos,
73 player: &Player,
74 _hand: InteractionHand,
75 _hit_result: &BlockHitResult,
76 inv: &mut InventoryAccess,
77 ) -> InteractionResult {
78 let content = inv.with_item(|item| item.item().block);
79 let Some(new_contents) = content.and_then(Self::potted_block_for) else {
80 return InteractionResult::TryEmptyHandInteraction;
81 };
82
83 if !self.is_empty() {
84 return InteractionResult::Consume;
85 }
86
87 world.set_block(pos, new_contents.default_state(), UpdateFlags::UPDATE_ALL);
88 world.game_event(
89 &vanilla_game_events::BLOCK_CHANGE,
90 pos,
91 &GameEventContext::new(Some(player), None),
92 );
93 player.award_custom_stat(&vanilla_custom_stats::POT_FLOWER);
94 let has_infinite_materials = player.has_infinite_materials();
95 inv.with_item(|item| item.consume_one(has_infinite_materials));
96 InteractionResult::Success
97 }
98
99 fn use_without_item(
100 &self,
101 _state: BlockStateId,
102 world: &Arc<World>,
103 pos: BlockPos,
104 player: &Player,
105 _hit_result: &BlockHitResult,
106 _inv: &mut InventoryAccess,
107 ) -> InteractionResult {
108 if self.is_empty() {
109 return InteractionResult::Consume;
110 }
111
112 player.add_item_or_drop(
113 ItemStack::new(REGISTRY.items.by_block(self.potted)),
114 Prediction::Predicted,
115 );
116 world.set_block(
117 pos,
118 vanilla_blocks::FLOWER_POT.default_state(),
119 UpdateFlags::UPDATE_ALL,
120 );
121 world.game_event(
122 &vanilla_game_events::BLOCK_CHANGE,
123 pos,
124 &GameEventContext::new(Some(player), None),
125 );
126 InteractionResult::Success
127 }
128
129 fn get_clone_item_stack(
130 &self,
131 block: BlockRef,
132 _state: BlockStateId,
133 _block_entity: Option<SharedBlockEntity>,
134 _include_data: bool,
135 ) -> Option<ItemStack> {
136 let item = if self.is_empty() {
137 REGISTRY.items.by_block(block)
138 } else {
139 REGISTRY.items.by_block(self.potted)
140 };
141 Some(ItemStack::new(item))
142 }
143
144 fn update_shape(
145 &self,
146 state: BlockStateId,
147 world: &dyn ScheduledTickAccess,
148 pos: BlockPos,
149 direction: Direction,
150 _neighbor_pos: BlockPos,
151 _neighbor_state: BlockStateId,
152 ) -> BlockStateId {
153 if direction == Direction::Down && !self.can_survive(state, world, pos) {
154 vanilla_blocks::AIR.default_state()
155 } else {
156 state
157 }
158 }
159
160 fn is_pathfindable(
161 &self,
162 _state: BlockStateId,
163 _computation_type: PathComputationType,
164 ) -> bool {
165 false
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use glam::DVec3;
172 use steel_registry::blocks::block_state_ext::BlockStateExt as _;
173 use steel_registry::{init_vanilla_registry, vanilla_items};
174 use steel_utils::{ChunkPos, Direction};
175
176 use super::*;
177 use crate::bootstrap::init_globals;
178 use crate::test_support::{
179 TestLevel, TestPlayerBuilder, fresh_test_world, insert_ready_full_chunk,
180 };
181
182 const TEST_POS: BlockPos = BlockPos::new(8, 64, 8);
183 const TEST_PLAYER_ENTITY_ID: i32 = 1;
184
185 fn empty_pot() -> FlowerPotBlock {
186 FlowerPotBlock::new(&vanilla_blocks::FLOWER_POT, &vanilla_blocks::AIR)
187 }
188
189 fn potted_dandelion() -> FlowerPotBlock {
190 FlowerPotBlock::new(
191 &vanilla_blocks::POTTED_DANDELION,
192 &vanilla_blocks::DANDELION,
193 )
194 }
195
196 fn interaction_hit() -> BlockHitResult {
197 BlockHitResult {
198 location: DVec3::new(8.5, 65.0, 8.5),
199 direction: Direction::Up,
200 block_pos: TEST_POS,
201 miss: false,
202 inside: false,
203 world_border_hit: false,
204 }
205 }
206
207 fn test_player(world: &Arc<World>) -> Arc<Player> {
208 TestPlayerBuilder::new(Arc::clone(world), "FlowerPotTester", TEST_PLAYER_ENTITY_ID).build()
209 }
210
211 #[test]
212 fn placing_and_removing_a_plant_swaps_the_potted_state() {
213 init_globals();
214 let world_fixture = fresh_test_world("flower_pot_interaction");
215 let world = &world_fixture.world;
216 insert_ready_full_chunk(world, ChunkPos::from_block_pos(TEST_POS));
217 let player = test_player(world);
218 let mut inventory =
219 InventoryAccess::new(Arc::clone(&player.inventory), InteractionHand::MainHand);
220
221 assert!(world.set_block(
222 TEST_POS,
223 vanilla_blocks::FLOWER_POT.default_state(),
224 UpdateFlags::UPDATE_ALL
225 ));
226
227 player
228 .inventory
229 .lock()
230 .set_selected_item(ItemStack::with_count(&vanilla_items::DANDELION, 1));
231 assert_eq!(
232 empty_pot().use_item_on(
233 world.get_block_state(TEST_POS),
234 world,
235 TEST_POS,
236 &player,
237 InteractionHand::MainHand,
238 &interaction_hit(),
239 &mut inventory,
240 ),
241 InteractionResult::Success,
242 );
243 assert_eq!(
244 world.get_block_state(TEST_POS).get_block(),
245 &vanilla_blocks::POTTED_DANDELION
246 );
247 assert!(player.inventory.lock().get_selected_item().is_empty());
248
249 player
250 .inventory
251 .lock()
252 .set_selected_item(ItemStack::new(&vanilla_items::STONE));
253 assert_eq!(
254 potted_dandelion().use_item_on(
255 world.get_block_state(TEST_POS),
256 world,
257 TEST_POS,
258 &player,
259 InteractionHand::MainHand,
260 &interaction_hit(),
261 &mut inventory,
262 ),
263 InteractionResult::TryEmptyHandInteraction,
264 );
265 assert_eq!(
266 potted_dandelion().use_without_item(
267 world.get_block_state(TEST_POS),
268 world,
269 TEST_POS,
270 &player,
271 &interaction_hit(),
272 &mut inventory,
273 ),
274 InteractionResult::Success,
275 );
276 assert_eq!(
277 world.get_block_state(TEST_POS).get_block(),
278 &vanilla_blocks::FLOWER_POT
279 );
280 assert!(
281 player
282 .inventory
283 .lock()
284 .get_items()
285 .iter()
286 .any(|item| item.is(&vanilla_items::DANDELION))
287 );
288
289 assert_eq!(
290 empty_pot().use_without_item(
291 world.get_block_state(TEST_POS),
292 world,
293 TEST_POS,
294 &player,
295 &interaction_hit(),
296 &mut inventory,
297 ),
298 InteractionResult::Consume,
299 );
300 }
301
302 #[test]
303 fn update_shape_leaves_a_floating_pot_intact() {
304 init_vanilla_registry();
305 let behavior = empty_pot();
306 let state = vanilla_blocks::FLOWER_POT.default_state();
307 let level = TestLevel::default();
308 assert_eq!(
309 behavior.update_shape(
310 state,
311 &level,
312 BlockPos::ZERO,
313 Direction::Down,
314 BlockPos::ZERO.below(),
315 vanilla_blocks::AIR.default_state(),
316 ),
317 state,
318 );
319 }
320
321 #[test]
322 fn clone_item_stack_returns_the_potted_plant() {
323 init_vanilla_registry();
324 let empty_clone = empty_pot()
325 .get_clone_item_stack(
326 &vanilla_blocks::FLOWER_POT,
327 vanilla_blocks::FLOWER_POT.default_state(),
328 None,
329 false,
330 )
331 .expect("flower pot always yields a clone stack");
332 assert!(empty_clone.is(&vanilla_items::FLOWER_POT));
333
334 let filled_clone = potted_dandelion()
335 .get_clone_item_stack(
336 &vanilla_blocks::POTTED_DANDELION,
337 vanilla_blocks::POTTED_DANDELION.default_state(),
338 None,
339 false,
340 )
341 .expect("potted flower pot always yields a clone stack");
342 assert!(filled_clone.is(&vanilla_items::DANDELION));
343 }
344}