steel_core/behavior/items/
brush.rs1use std::sync::Arc;
4
5use steel_macros::item_behavior;
6use steel_registry::item_stack::ItemStack;
7use steel_registry::sound_events;
8use steel_utils::Downcast as _;
9use steel_utils::types::InteractionHand;
10use steel_utils::{BlockPos, Direction};
11
12use crate::behavior::context::{InteractionResult, UseOnContext};
13use crate::behavior::{BLOCK_BEHAVIORS, ItemBehavior, ItemUseAnimation};
14use crate::block_entity::entities::BrushableBlockEntity;
15use crate::entity::projectile::{ViewVectorHitResult, get_hit_result_on_view_vector};
16use crate::entity::{Entity, LivingEntity};
17use crate::inventory::equipment::EquipmentSlot;
18use crate::player::Player;
19use crate::world::World;
20
21const USE_DURATION: i32 = 200;
22const ANIMATION_DURATION: i32 = 10;
23const SOUND_TICK_OFFSET: i32 = 5;
24
25#[item_behavior]
27pub struct BrushItem;
28
29impl ItemBehavior for BrushItem {
30 fn use_on(&self, context: &mut UseOnContext) -> InteractionResult {
31 if calculate_block_hit(context.world, context.player).is_some() {
32 context.player.start_using_item(context.hand);
33 }
34 InteractionResult::Consume
35 }
36
37 fn get_use_duration(&self, _stack: &ItemStack, _user: &dyn LivingEntity) -> i32 {
38 USE_DURATION
39 }
40
41 fn get_use_animation(&self, _stack: &ItemStack) -> ItemUseAnimation {
42 ItemUseAnimation::Brush
43 }
44
45 fn on_use_tick(
46 &self,
47 world: &Arc<World>,
48 user: &dyn LivingEntity,
49 stack: &mut ItemStack,
50 ticks_remaining: i32,
51 ) {
52 if ticks_remaining < 0 {
53 release_player_use(user);
54 return;
55 }
56
57 let Some(player) = user.as_player() else {
58 return;
59 };
60 let Some((pos, direction)) = calculate_block_hit(world, player) else {
61 player.release_using_item();
62 return;
63 };
64
65 let elapsed = USE_DURATION - ticks_remaining + 1;
66 if elapsed % ANIMATION_DURATION != SOUND_TICK_OFFSET {
67 return;
68 }
69
70 let state = world.get_block_state(pos);
71 let sound = BLOCK_BEHAVIORS
72 .get_behavior_for_state(state)
73 .and_then(|behavior| behavior.brushable_data(state))
74 .map_or(&sound_events::ITEM_BRUSH_BRUSHING_GENERIC, |data| {
75 data.brush_sound
76 });
77 world.play_block_sound(sound, pos, 1.0, 1.0, Some(player.id()));
78
79 let Some(block_entity) = world.get_block_entity(pos) else {
80 return;
81 };
82 let Some(brushable) = block_entity.downcast_ref::<BrushableBlockEntity>() else {
83 return;
84 };
85 let outcome = brushable.brush(world.game_time(), world, player, direction, stack);
86 outcome.mutation.apply(world, pos);
87
88 if outcome.durability_damage {
89 let slot = equipped_brush_slot(player, stack);
90 let item_ref = stack.item;
91 if stack.hurt_and_break(1, player.has_infinite_materials()) {
92 player.on_equipped_item_broken(item_ref, slot);
93 }
94 }
95 }
96}
97
98fn calculate_block_hit(world: &World, player: &Player) -> Option<(BlockPos, Direction)> {
99 let distance = player.block_interaction_range();
100 match get_hit_result_on_view_vector(world, player, distance, Entity::is_pickable) {
101 ViewVectorHitResult::Block(hit) => Some((hit.block_pos, hit.direction)),
102 ViewVectorHitResult::Miss | ViewVectorHitResult::Entity(_) => None,
103 }
104}
105
106fn equipped_brush_slot(player: &Player, _stack: &ItemStack) -> EquipmentSlot {
107 match player.active_item_use_hand() {
108 Some(InteractionHand::OffHand) => EquipmentSlot::OffHand,
109 Some(InteractionHand::MainHand) | None => EquipmentSlot::MainHand,
110 }
111}
112
113fn release_player_use(user: &dyn LivingEntity) {
114 if let Some(player) = user.as_player() {
115 player.release_using_item();
116 }
117}