Skip to main content

steel_core/behavior/items/
bonemeal.rs

1use std::sync::Arc;
2
3use rand::RngExt;
4use steel_macros::item_behavior;
5use steel_registry::{
6    blocks::{block_state_ext::BlockStateExt, shapes::is_offset_shape_full_block},
7    data_components::vanilla_components::USE_EFFECTS,
8    item_stack::ItemStack,
9    level_events, vanilla_blocks, vanilla_game_events,
10};
11use steel_utils::{BlockPos, Direction, types::UpdateFlags};
12
13use crate::{
14    behavior::{BLOCK_BEHAVIORS, InteractionResult, ItemBehavior, UseOnContext},
15    entity::Entity,
16    world::{LevelReader as _, World},
17};
18
19/// Behavior for the Bonemeal item.
20#[item_behavior]
21pub struct BoneMealItem;
22
23impl BoneMealItem {
24    fn cause_finish_use_vibration(context: &UseOnContext<'_>) {
25        let interact_vibrations = context.inv.with_item(|item| {
26            item.get(USE_EFFECTS)
27                .is_some_and(|effects| effects.interact_vibrations)
28        });
29        if interact_vibrations {
30            context
31                .player
32                .game_event(&vanilla_game_events::ITEM_INTERACT_FINISH);
33        }
34    }
35
36    fn grow(world: &Arc<World>, pos: BlockPos) -> bool {
37        let state = world.get_block_state(pos);
38        let Some(behavior) = BLOCK_BEHAVIORS.get_behavior_for_state(state) else {
39            return false;
40        };
41        if let Some(bonemealable) = behavior.as_bonemealable() {
42            if !bonemealable.is_valid_bonemeal_target(state, world.as_ref(), pos) {
43                return false;
44            }
45
46            let mut rng = rand::rng();
47            if bonemealable.is_bonemeal_success(state, world, &mut rng, pos) {
48                bonemealable.perform_bonemeal(state, world, &mut rng, pos);
49            }
50
51            return true;
52        }
53        false
54    }
55
56    fn grow_water_plant(world: &Arc<World>, pos: BlockPos, _clicked_face: Direction) -> bool {
57        let state = world.get_block_state(pos);
58        if state.get_block() != &vanilla_blocks::WATER || !state.get_fluid_state().is_full() {
59            return false;
60        }
61
62        let Some(bonemealable) = BLOCK_BEHAVIORS
63            .get_behavior(&vanilla_blocks::SEAGRASS)
64            .as_bonemealable()
65        else {
66            return false;
67        };
68
69        let mut rng = rand::rng();
70
71        'outer: for i in 0..128 {
72            let mut new_pos = pos;
73            let new_state = vanilla_blocks::SEAGRASS.default_state();
74
75            for _ in 0..(i / 16) {
76                new_pos = new_pos.offset(
77                    rng.random_range(0i32..3) - 1,
78                    (rng.random_range(0i32..3) - 1) * rng.random_range(0i32..3) / 2,
79                    rng.random_range(0i32..3) - 1,
80                );
81
82                if is_offset_shape_full_block(
83                    world
84                        .get_block_state(new_pos)
85                        .get_collision_shape_at(new_pos),
86                ) {
87                    continue 'outer;
88                }
89            }
90
91            // TODO: implement coral and underwater bonemeal tag selection.
92
93            let Some(behavior) = BLOCK_BEHAVIORS.get_behavior_for_state(new_state) else {
94                return false;
95            };
96
97            if behavior.can_survive(new_state, world, new_pos) {
98                let current_state = world.get_block_state(new_pos);
99                if current_state.get_block() == &vanilla_blocks::WATER
100                    && current_state.get_fluid_state().is_full()
101                {
102                    world.set_block(new_pos, new_state, UpdateFlags::UPDATE_ALL);
103                } else if current_state.get_block() == &vanilla_blocks::SEAGRASS
104                    && bonemealable.is_valid_bonemeal_target(current_state, world.as_ref(), new_pos)
105                    && rng.random_range(0..10) == 0
106                {
107                    bonemealable.perform_bonemeal(current_state, world, &mut rng, new_pos);
108                }
109            }
110        }
111
112        true
113    }
114}
115
116impl ItemBehavior for BoneMealItem {
117    fn use_on(&self, context: &mut UseOnContext) -> InteractionResult {
118        if Self::grow(context.world, context.hit_result.block_pos) {
119            context.inv.with_item(ItemStack::shrink_one);
120            Self::cause_finish_use_vibration(context);
121            context.world.level_event(
122                level_events::PARTICLES_AND_SOUND_PLANT_GROWTH,
123                context.hit_result.block_pos,
124                15,
125                None,
126            );
127            return InteractionResult::Success;
128        }
129        let state = context.world.get_block_state(context.hit_result.block_pos);
130        let is_clicked_face_sturdy = context.world.is_face_sturdy(
131            state,
132            context.hit_result.block_pos,
133            context.hit_result.direction,
134        );
135        if is_clicked_face_sturdy
136            && Self::grow_water_plant(
137                context.world,
138                context
139                    .hit_result
140                    .block_pos
141                    .relative(context.hit_result.direction),
142                context.hit_result.direction,
143            )
144        {
145            context.inv.with_item(ItemStack::shrink_one);
146            Self::cause_finish_use_vibration(context);
147            context.world.level_event(
148                level_events::PARTICLES_AND_SOUND_PLANT_GROWTH,
149                context
150                    .hit_result
151                    .block_pos
152                    .relative(context.hit_result.direction),
153                15,
154                None,
155            );
156            return InteractionResult::Success;
157        }
158        InteractionResult::Pass
159    }
160}