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