Skip to main content

steel_core/behavior/items/
bottle.rs

1//! Glass bottle item behavior (`BottleItem`).
2//!
3//! Filling from a water source produces a water potion via
4//! `ItemUtils.createFilledResult`. Dragon-breath filling is omitted until
5//! area-effect clouds exist.
6
7use 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/// Behavior for the glass bottle item.
23#[item_behavior(class = "BottleItem")]
24pub struct BottleItem;
25
26impl ItemBehavior for BottleItem {
27    fn use_item(&self, context: &mut UseItemContext) -> InteractionResult {
28        // Vanilla `BottleItem.use` checks dragon-breath area-effect clouds first.
29        // Steel has no `AreaEffectCloud` yet, so water filling is the live path.
30
31        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}