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::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/// Behavior for the glass bottle item.
27#[item_behavior(class = "BottleItem")]
28pub struct BottleItem;
29
30impl ItemBehavior for BottleItem {
31    fn use_item(&self, context: &mut UseItemContext) -> InteractionResult {
32        // Vanilla `BottleItem.use` checks dragon-breath area-effect clouds first.
33        // Steel has no `AreaEffectCloud` yet, so water filling is the live path.
34
35        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
75/// Vanilla `PotionContents.createItemStack(Items.POTION, Potions.WATER)`.
76fn 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}