steel_core/behavior/items/
bucket.rs1use crate::behavior::context::InteractionResult;
9use crate::behavior::item_utils::create_filled_result;
10use crate::behavior::{
11 BLOCK_BEHAVIORS, BlockStateBehaviorExt, FLUID_BEHAVIORS, ItemBehavior, UseItemContext,
12 pickup_waterlogged_block,
13};
14use crate::entity::Entity;
15use crate::fluid::FluidStateExt;
16use crate::world::RaytraceAction;
17use steel_macros::item_behavior;
18use steel_registry::blocks::BlockRef;
19use steel_registry::blocks::block_state_ext::BlockStateExt;
20use steel_registry::blocks::properties::Direction;
21use steel_registry::fluid::FluidState;
22use steel_registry::item_stack::ItemStack;
23use steel_registry::level_events;
24use steel_registry::sound_events;
25use steel_registry::stat::vanilla_stat_types;
26use steel_registry::vanilla_blocks;
27use steel_registry::vanilla_fluids;
28use steel_registry::vanilla_game_events;
29use steel_registry::vanilla_items;
30use steel_utils::types::UpdateFlags;
31use steel_utils::{BlockPos, BlockStateId};
32
33use crate::world::game_event::GameEventContext;
34
35#[item_behavior]
37pub struct BucketItem {
38 #[json_arg(vanilla_blocks, json = "content", optional = "empty")]
39 fluid_block: Option<BlockRef>,
40}
41
42impl BucketItem {
43 #[must_use]
45 pub const fn new(fluid_block: Option<BlockRef>) -> Self {
46 Self { fluid_block }
47 }
48}
49
50impl ItemBehavior for BucketItem {
51 fn use_item(&self, context: &mut UseItemContext) -> InteractionResult {
52 match self.fluid_block {
53 None => use_empty_bucket(context),
54 Some(fluid_block) => use_filled_bucket(fluid_block, context),
55 }
56 }
57}
58
59fn filled_bucket_success_stack(context: &UseItemContext) -> ItemStack {
60 if context.player.has_infinite_materials() {
61 context
62 .inv
63 .with_item(|item| item.copy_with_count(item.count()))
64 } else {
65 ItemStack::new(&vanilla_items::BUCKET)
66 }
67}
68
69fn use_empty_bucket(context: &mut UseItemContext) -> InteractionResult {
70 let (start, end) = context.player.get_ray_endpoints();
71
72 let (hit_block, _) = context.world.raytrace(start, end, |pos, world| {
74 let state = world.get_block_state(pos);
75 let block = state.get_block();
76
77 if block == &vanilla_blocks::AIR {
78 return RaytraceAction::Pass;
79 }
80
81 let fluid_state = state.get_fluid_state();
82 if fluid_state.is_source() {
83 return RaytraceAction::ImmediateHit;
84 }
85 if !fluid_state.is_empty() {
87 return RaytraceAction::Pass;
88 }
89
90 RaytraceAction::CheckShape
91 });
92
93 let Some(hit_pos) = hit_block else {
95 return InteractionResult::Pass;
96 };
97
98 let hit_state = context.world.get_block_state(hit_pos);
99 let block_behavior = BLOCK_BEHAVIORS.get_behavior(hit_state.get_block());
100
101 if let Some(result) =
102 block_behavior.pickup_block(context.world, hit_pos, hit_state, Some(context.player))
103 {
104 context.inv.with_item(|item| {
105 context
106 .player
107 .award_stat(&vanilla_stat_types::ITEM_USED, item.item());
108 });
109 if let Some(sound) = result.sound {
111 context.player.play_sound(sound, 1.0, 1.0);
113 }
114
115 create_filled_result(context, result.filled_bucket, true);
117 context.world.game_event(
118 &vanilla_game_events::FLUID_PICKUP,
119 hit_pos,
120 &GameEventContext::new(Some(context.player), None),
121 );
122
123 return InteractionResult::Success;
124 }
125
126 if let Some(result) = pickup_waterlogged_block(
128 block_behavior,
129 context.world,
130 hit_pos,
131 hit_state,
132 Some(context.player),
133 ) {
134 context.inv.with_item(|item| {
135 context
136 .player
137 .award_stat(&vanilla_stat_types::ITEM_USED, item.item());
138 });
139 if let Some(sound) = result.sound {
140 context.player.play_sound(sound, 1.0, 1.0);
142 }
143
144 create_filled_result(context, result.filled_bucket, true);
145 context.world.game_event(
146 &vanilla_game_events::FLUID_PICKUP,
147 hit_pos,
148 &GameEventContext::new(Some(context.player), None),
149 );
150
151 return InteractionResult::Success;
152 }
153
154 InteractionResult::Fail
157}
158
159#[expect(
161 clippy::too_many_lines,
162 reason = "mirrors vanilla's emptyContents flow; splitting would obscure the sequential placement logic"
163)]
164fn use_filled_bucket(fluid_block: BlockRef, context: &mut UseItemContext) -> InteractionResult {
165 let (start, end) = context.player.get_ray_endpoints();
167 let (ray_block, ray_dir) = context.world.raytrace(start, end, |pos, world| {
168 let state = world.get_block_state(pos);
169 let block = state.get_block();
170 if block == &vanilla_blocks::AIR {
173 return RaytraceAction::Pass;
174 }
175 RaytraceAction::CheckShape
176 });
177
178 let (Some(clicked_pos), Some(direction)) = (ray_block, ray_dir) else {
180 return InteractionResult::Pass;
181 };
182
183 if !context.world.is_in_valid_bounds(clicked_pos) {
185 return InteractionResult::Fail;
186 }
187
188 let clicked_state = context.world.get_block_state(clicked_pos);
189 let is_sneaking = context.player.is_crouching();
190
191 let try_place_fluid = |pos: BlockPos, check_sneak: bool| -> bool {
195 if !context.world.is_in_valid_bounds(pos) {
196 return false;
197 }
198
199 let state = context.world.get_block_state(pos);
200 let fluid_state = state.get_fluid_state();
201
202 if check_sneak && is_sneaking && !state.get_block().config.is_air {
206 return false;
207 }
208
209 let is_water_bucket = fluid_block == &vanilla_blocks::WATER;
210 let behavior = BLOCK_BEHAVIORS.get_behavior(state.get_block());
211 let is_liquid_container = state.is_liquid_container();
212 let can_place_liquid = is_water_bucket
213 && is_liquid_container
214 && behavior.can_place_liquid_with_player(
215 state,
216 FluidState::source(&vanilla_fluids::WATER).fluid_id,
217 Some(context.player),
218 );
219 let can_replace = state.can_be_replaced_by_fluid(fluid_block);
220
221 if !can_replace && !can_place_liquid {
223 return false;
224 }
225
226 if is_water_bucket && context.world.dimension_type.water_evaporates {
230 context
231 .world
232 .level_event(level_events::PARTICLES_WATER_EVAPORATING, pos, 0, None);
233 return true;
234 }
235
236 if is_water_bucket && is_liquid_container {
238 let source_water = FluidState::source(&vanilla_fluids::WATER);
239 behavior.place_liquid(context.world, pos, state, source_water);
240 play_empty_sound_and_event(context, pos, true);
241 return true;
242 }
243
244 if can_replace {
246 let is_same_fluid = if is_water_bucket {
248 fluid_state.is_water()
249 } else {
250 fluid_state.is_lava()
251 };
252
253 if is_same_fluid && fluid_state.is_source() {
254 play_empty_sound_and_event(context, pos, is_water_bucket);
255 return true;
256 }
257
258 if !state.get_block().config.liquid && !state.get_block().config.is_air {
261 context.player.get_world().destroy_block(pos, true);
262 }
263
264 let fluid_state_to_place = fluid_block.default_state();
266 if context
267 .world
268 .set_block(pos, fluid_state_to_place, UpdateFlags::UPDATE_ALL_IMMEDIATE)
269 {
270 let fluid_ref = if is_water_bucket {
271 &vanilla_fluids::WATER
272 } else {
273 &vanilla_fluids::LAVA
274 };
275 let tick_delay = FLUID_BEHAVIORS
276 .get_behavior(fluid_ref)
277 .tick_delay(context.world);
278 context
279 .world
280 .schedule_fluid_tick_default(pos, fluid_ref, tick_delay);
281
282 play_empty_sound_and_event(context, pos, is_water_bucket);
283
284 return true;
285 }
286 }
287 false
288 };
289
290 let is_water_bucket = fluid_block == &vanilla_blocks::WATER;
295 let primary_pos =
296 filled_bucket_primary_pos(clicked_state, clicked_pos, direction, is_water_bucket);
297
298 if try_place_fluid(primary_pos, true) {
300 let result_stack = filled_bucket_success_stack(context);
301 create_filled_result(context, result_stack, true);
302 return InteractionResult::Success;
303 }
304
305 let secondary_pos = direction.relative(clicked_pos);
309 if try_place_fluid(secondary_pos, false) {
310 let result_stack = filled_bucket_success_stack(context);
311 create_filled_result(context, result_stack, true);
312 return InteractionResult::Success;
313 }
314
315 InteractionResult::Fail
316}
317
318fn play_empty_sound_and_event(context: &UseItemContext, pos: BlockPos, is_water_bucket: bool) {
319 let sound_event = if is_water_bucket {
320 &sound_events::ITEM_BUCKET_EMPTY
321 } else {
322 &sound_events::ITEM_BUCKET_EMPTY_LAVA
323 };
324 context.world.play_block_sound(
325 sound_event,
326 pos,
327 1.0,
328 1.0,
329 Some(context.player.id()),
331 );
332 context.world.game_event(
333 &vanilla_game_events::FLUID_PLACE,
334 pos,
335 &GameEventContext::new(Some(context.player), None),
336 );
337}
338
339fn filled_bucket_primary_pos(
340 clicked_state: BlockStateId,
341 clicked_pos: BlockPos,
342 direction: Direction,
343 is_water_bucket: bool,
344) -> BlockPos {
345 if is_water_bucket && clicked_state.is_liquid_container() {
346 clicked_pos
347 } else {
348 direction.relative(clicked_pos)
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use crate::behavior::init_behaviors;
355 use steel_registry::{init_vanilla_registry, vanilla_blocks};
356
357 use super::*;
358
359 #[test]
360 fn filled_water_bucket_targets_non_waterlogged_liquid_container_in_place() {
361 init_vanilla_registry();
362 init_behaviors();
363
364 let kelp = vanilla_blocks::KELP.default_state();
365
366 assert_eq!(
367 filled_bucket_primary_pos(kelp, BlockPos::ZERO, Direction::North, true),
368 BlockPos::ZERO
369 );
370 assert_eq!(
371 filled_bucket_primary_pos(kelp, BlockPos::ZERO, Direction::North, false),
372 Direction::North.relative(BlockPos::ZERO)
373 );
374 }
375}