steel_core/behavior/items/
bottle.rs1use steel_macros::item_behavior;
8use steel_protocol::packets::game::SoundSource;
9use steel_registry::blocks::block_state_ext::BlockStateExt;
10use steel_registry::data_components::PotionContents;
11use steel_registry::stat::vanilla_stat_types;
12use steel_registry::{sound_events, vanilla_game_events, vanilla_items, vanilla_potions};
13
14use crate::behavior::context::{InteractionResult, UseItemContext};
15use crate::behavior::item::ItemBehavior;
16use crate::behavior::item_utils::{create_filled_result, get_player_pov_hit_result};
17use crate::entity::Entity;
18use crate::fluid::FluidStateExt;
19use crate::world::ClipFluid;
20use crate::world::game_event::GameEventContext;
21
22#[item_behavior(class = "BottleItem")]
24pub struct BottleItem;
25
26impl ItemBehavior for BottleItem {
27 fn use_item(&self, context: &mut UseItemContext) -> InteractionResult {
28 let hit = get_player_pov_hit_result(context.world, context.player, ClipFluid::SourceOnly);
32 if hit.miss {
33 return InteractionResult::Pass;
34 }
35
36 let pos = hit.block_pos;
37 if !context.world.may_interact(context.player, pos) {
38 return InteractionResult::Pass;
39 }
40
41 let fluid_state = context.world.get_block_state(pos).get_fluid_state();
42 if !fluid_state.is_water() {
43 return InteractionResult::Pass;
44 }
45
46 context.world.play_sound_at(
47 &sound_events::ITEM_BOTTLE_FILL,
48 SoundSource::Neutral,
49 context.player.position(),
50 1.0,
51 1.0,
52 Some(context.player.id()),
53 );
54 context.world.game_event(
55 &vanilla_game_events::FLUID_PICKUP,
56 pos,
57 &GameEventContext::new(Some(context.player.as_ref()), None),
58 );
59
60 context.inv.with_item(|item| {
61 context
62 .player
63 .award_stat(&vanilla_stat_types::ITEM_USED, item.item());
64 });
65 create_filled_result(
66 context,
67 PotionContents::create_item_stack(&vanilla_items::POTION, &vanilla_potions::WATER),
68 true,
69 );
70
71 InteractionResult::Success
72 }
73}
74
75#[cfg(test)]
76mod tests {
77 use std::sync::Arc;
78
79 use glam::DVec3;
80 use steel_registry::blocks::block_state_ext::BlockStateExt;
81 use steel_registry::data_components::vanilla_components::POTION_CONTENTS;
82 use steel_registry::item_stack::ItemStack;
83 use steel_registry::{init_vanilla_registry, vanilla_blocks, vanilla_items, vanilla_potions};
84 use steel_utils::types::{InteractionHand, UpdateFlags};
85 use steel_utils::{BlockPos, BlockStateId, ChunkPos};
86
87 use crate::behavior::item::ItemBehavior;
88 use crate::behavior::{InteractionResult, UseItemContext, init_behaviors};
89 use crate::entity::Entity;
90 use crate::player::Player;
91 use crate::test_support::{TestPlayerBuilder, fresh_test_world, insert_ready_full_chunk};
92 use crate::world::World;
93
94 use super::BottleItem;
95
96 fn looking_down_at(
97 world: &Arc<World>,
98 feet: DVec3,
99 target: BlockPos,
100 state: BlockStateId,
101 ) -> Arc<Player> {
102 insert_ready_full_chunk(world, ChunkPos::from_block_pos(target));
103 if world.get_block_state(target) != state {
104 assert!(world.set_block(target, state, UpdateFlags::UPDATE_NONE));
105 }
106
107 let player = TestPlayerBuilder::new(Arc::clone(world), "BottleTester", 1).build();
108 player
109 .try_set_position(feet)
110 .expect("test player should move onto the target chunk");
111 player.set_rotation((0.0, 90.0));
112 player
113 }
114
115 fn use_bottle(player: &Arc<Player>, world: &Arc<World>, count: i32) -> InteractionResult {
116 player.inventory.lock().set_item_in_hand(
117 InteractionHand::MainHand,
118 ItemStack::with_count(&vanilla_items::GLASS_BOTTLE, count),
119 );
120 let mut context = UseItemContext::new(
121 player,
122 InteractionHand::MainHand,
123 world,
124 Arc::clone(&player.inventory),
125 );
126 BottleItem.use_item(&mut context)
127 }
128
129 fn hand_item(player: &Player) -> ItemStack {
130 player
131 .inventory
132 .lock()
133 .get_item_in_hand(InteractionHand::MainHand)
134 .clone()
135 }
136
137 #[test]
138 fn fills_from_a_water_source_and_leaves_the_source() {
139 init_vanilla_registry();
140 init_behaviors();
141
142 let world_fixture = fresh_test_world("bottle_fill_water");
143 let world = &world_fixture.world;
144 let water_pos = BlockPos::new(0, 80, 0);
145 let player = looking_down_at(
146 world,
147 DVec3::new(0.5, 81.0, 0.5),
148 water_pos,
149 vanilla_blocks::WATER.default_state(),
150 );
151
152 assert_eq!(use_bottle(&player, world, 1), InteractionResult::Success);
153
154 let filled = hand_item(&player);
155 assert_eq!(filled.item.key, vanilla_items::POTION.key);
156 assert!(
157 filled
158 .get(POTION_CONTENTS)
159 .is_some_and(|contents| contents.is(&vanilla_potions::WATER))
160 );
161 assert_eq!(
162 world.get_block_state(water_pos).get_block(),
163 &vanilla_blocks::WATER
164 );
165 }
166
167 #[test]
168 fn non_water_blocks_and_misses_do_not_fill() {
169 init_vanilla_registry();
170 init_behaviors();
171
172 let world_fixture = fresh_test_world("bottle_fill_stone");
173 let world = &world_fixture.world;
174 let player = looking_down_at(
175 world,
176 DVec3::new(0.5, 81.0, 0.5),
177 BlockPos::new(0, 80, 0),
178 vanilla_blocks::STONE.default_state(),
179 );
180
181 assert_eq!(use_bottle(&player, world, 1), InteractionResult::Pass);
182 assert_eq!(hand_item(&player).item.key, vanilla_items::GLASS_BOTTLE.key);
183
184 let air_world_fixture = fresh_test_world("bottle_fill_air");
185 let air_world = &air_world_fixture.world;
186 let air_player = looking_down_at(
187 air_world,
188 DVec3::new(0.5, 81.0, 0.5),
189 BlockPos::new(0, 80, 0),
190 vanilla_blocks::AIR.default_state(),
191 );
192 assert_eq!(
193 use_bottle(&air_player, air_world, 1),
194 InteractionResult::Pass
195 );
196 }
197}