Skip to main content

steel_core/behavior/items/
fishing_rod.rs

1use crate::behavior::{InteractionResult, ItemBehavior, UseItemContext};
2use crate::entity::entities::objects::projectiles::FishingHookEntity;
3use crate::entity::{Entity, RemovalReason, SharedEntity, next_entity_id};
4use glam::DVec3;
5use rand::{RngExt, rng};
6use std::sync::Arc;
7use steel_macros::item_behavior;
8use steel_protocol::packets::game::SoundSource;
9use steel_registry::data_components::vanilla_components::USE_EFFECTS;
10use steel_registry::enchantment_effect::EnchantmentEffectComponent;
11use steel_registry::game_events::GameEventRef;
12use steel_registry::sound_events::{ENTITY_FISHING_BOBBER_RETRIEVE, ENTITY_FISHING_BOBBER_THROW};
13use steel_registry::stat::vanilla_stat_types;
14use steel_registry::{vanilla_entities, vanilla_game_events, vanilla_items};
15
16/// Behavior for the fishing rod item.
17#[item_behavior]
18pub struct FishingRodItem;
19
20impl FishingRodItem {
21    /// Emits `event` when the rod's `use_effects` enable interaction vibrations.
22    fn cause_use_vibration(context: &UseItemContext, event: GameEventRef) {
23        let interact_vibrations = context.inv.with_item(|item| {
24            item.get(USE_EFFECTS)
25                .is_some_and(|effects| effects.interact_vibrations)
26        });
27        if interact_vibrations {
28            context.player.game_event(event);
29        }
30    }
31}
32
33impl ItemBehavior for FishingRodItem {
34    fn use_item(&self, context: &mut UseItemContext) -> InteractionResult {
35        let player = context.player;
36        let infinite_materials = context.player.has_infinite_materials();
37        let world = context.world;
38        let item = context.inv.with_item(|item| item.clone());
39
40        let pitch = 0.4 / rng().random_range(0.8..1.2);
41
42        if let Some(fishing) = player.fishing_hook() {
43            if item.is(&vanilla_items::FISHING_ROD) {
44                let damage = fishing.retrieve(&item);
45
46                context.inv.with_item(|item| {
47                    item.hurt_and_break(damage, infinite_materials);
48                });
49            }
50
51            world.play_sound_at(
52                &ENTITY_FISHING_BOBBER_RETRIEVE,
53                SoundSource::Neutral,
54                player.position(),
55                1.0,
56                pitch,
57                None,
58            );
59            Self::cause_use_vibration(context, &vanilla_game_events::ITEM_INTERACT_FINISH);
60        } else {
61            world.play_sound_at(
62                &ENTITY_FISHING_BOBBER_THROW,
63                SoundSource::Neutral,
64                player.position(),
65                0.5,
66                pitch,
67                None,
68            );
69
70            let player_pos = player.position();
71            let spawn_pos = DVec3::new(player_pos.x, player.get_eye_y() - 0.1, player_pos.z);
72
73            let luck = item.apply_unconditional_enchantment_value_effects(
74                EnchantmentEffectComponent::FishingLuckBonus,
75                0.0,
76            );
77
78            let lure_speed = item.apply_unconditional_enchantment_value_effects(
79                EnchantmentEffectComponent::FishingTimeReduction,
80                0.0,
81            );
82
83            let hook = Arc::new(FishingHookEntity::new(
84                &vanilla_entities::FISHING_BOBBER,
85                next_entity_id(),
86                spawn_pos,
87                Arc::downgrade(world),
88            ));
89
90            let player_arc = world
91                .players
92                .get_by_uuid(&player.gameprofile.id())
93                .expect("Failed to obtain `player_arc` for launching fishing hook: `player` must be registered in the world.");
94
95            hook.shoot_from_player(&player_arc, luck as i32, lure_speed as i32);
96
97            let entity: SharedEntity = hook;
98
99            if let Err(error) = world.try_add_entity(Arc::clone(&entity)) {
100                entity.set_removed(RemovalReason::Discarded);
101                log::error!("Failed to spawn fishing hook: {error}");
102                return InteractionResult::Fail;
103            }
104
105            player.award_stat(&vanilla_stat_types::ITEM_USED, item.item());
106
107            Self::cause_use_vibration(context, &vanilla_game_events::ITEM_INTERACT_START);
108        }
109        InteractionResult::Success
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use steel_registry::{item_stack::ItemStack, vanilla_items};
116    use steel_utils::locks::SyncMutex;
117    use steel_utils::types::InteractionHand;
118    use steel_utils::{BlockPos, ChunkPos, SectionPos};
119    use uuid::Uuid;
120
121    use super::*;
122    use crate::behavior::UseItemContext;
123    use crate::test_support::{TestPlayerBuilder, fresh_test_world, insert_ready_full_chunk};
124    use crate::world::World;
125    use crate::world::game_event::{GameEventContext, GameEventListener};
126
127    struct RecordingGameEventListener(Arc<SyncMutex<Vec<GameEventRef>>>);
128
129    impl GameEventListener for RecordingGameEventListener {
130        fn listener_pos(&self) -> Option<DVec3> {
131            Some(DVec3::new(8.5, 64.5, 8.5))
132        }
133
134        fn listener_radius(&self) -> i32 {
135            16
136        }
137
138        fn handle_game_event(
139            &self,
140            _world: &Arc<World>,
141            event: GameEventRef,
142            _context: &GameEventContext<'_>,
143            _source_pos: DVec3,
144        ) -> bool {
145            self.0.lock().push(event);
146            true
147        }
148    }
149
150    #[test]
151    fn retrieving_grounded_hook_does_not_relock_inventory() {
152        let world_fixture = fresh_test_world("fishing_rod_grounded_retrieve");
153        let world = &world_fixture.world;
154        let player = TestPlayerBuilder::new(Arc::clone(world), "Player", 1)
155            .uuid(Uuid::from_u128(1))
156            .build();
157        player
158            .inventory
159            .lock()
160            .set_selected_item(ItemStack::new(&vanilla_items::FISHING_ROD));
161
162        let player_owner = Arc::clone(&player);
163        let owner: SharedEntity = player_owner;
164        let hook = Arc::new(FishingHookEntity::new(
165            &vanilla_entities::FISHING_BOBBER,
166            2,
167            DVec3::ZERO,
168            Arc::downgrade(world),
169        ));
170        hook.set_owner(&owner);
171        hook.set_on_ground(true);
172
173        let mut context = UseItemContext::new(
174            &player,
175            InteractionHand::MainHand,
176            world,
177            Arc::clone(&player.inventory),
178        );
179
180        assert_eq!(
181            FishingRodItem.use_item(&mut context),
182            InteractionResult::Success
183        );
184        assert!(hook.is_removed());
185        assert!(player.fishing_hook().is_none());
186        assert_eq!(context.inv.with_item(|item| item.get_damage_value()), 2);
187    }
188
189    #[test]
190    fn use_emits_vanilla_interact_vibrations() {
191        let world_fixture = fresh_test_world("fishing_rod_vibrations");
192        let world = &world_fixture.world;
193        insert_ready_full_chunk(world, ChunkPos::new(0, 0));
194        let player = TestPlayerBuilder::new(Arc::clone(world), "Player", 11)
195            .uuid(Uuid::from_u128(3))
196            .build();
197        player
198            .try_set_position(DVec3::new(8.0, 64.0, 8.0))
199            .expect("should position player in center of chunk");
200        world.players.insert(Arc::clone(&player));
201        player
202            .inventory
203            .lock()
204            .set_selected_item(ItemStack::new(&vanilla_items::FISHING_ROD));
205
206        let events = Arc::new(SyncMutex::new(Vec::new()));
207        world.register_game_event_listener(
208            SectionPos::from_block_pos(BlockPos::new(8, 64, 8)),
209            Arc::new(RecordingGameEventListener(Arc::clone(&events))),
210        );
211
212        let mut context = UseItemContext::new(
213            &player,
214            InteractionHand::MainHand,
215            world,
216            Arc::clone(&player.inventory),
217        );
218
219        assert_eq!(
220            FishingRodItem.use_item(&mut context),
221            InteractionResult::Success
222        );
223        assert_eq!(
224            events.lock().as_slice(),
225            [&vanilla_game_events::ITEM_INTERACT_START]
226        );
227
228        assert_eq!(
229            FishingRodItem.use_item(&mut context),
230            InteractionResult::Success
231        );
232        assert_eq!(
233            events.lock().as_slice(),
234            [
235                &vanilla_game_events::ITEM_INTERACT_START,
236                &vanilla_game_events::ITEM_INTERACT_FINISH,
237            ]
238        );
239    }
240
241    #[test]
242    fn casting_fishing_rod_spawns_and_links_hook() {
243        let world_fixture = fresh_test_world("fishing_rod_cast");
244        let world = &world_fixture.world;
245        insert_ready_full_chunk(world, ChunkPos::new(0, 0));
246        let player = TestPlayerBuilder::new(Arc::clone(world), "Player", 10)
247            .uuid(Uuid::from_u128(2))
248            .build();
249        player
250            .try_set_position(DVec3::new(8.0, 64.0, 8.0))
251            .expect("should position player in center of chunk");
252        world.players.insert(Arc::clone(&player));
253        player
254            .inventory
255            .lock()
256            .set_selected_item(ItemStack::new(&vanilla_items::FISHING_ROD));
257
258        let mut context = UseItemContext::new(
259            &player,
260            InteractionHand::MainHand,
261            world,
262            Arc::clone(&player.inventory),
263        );
264
265        assert_eq!(
266            FishingRodItem.use_item(&mut context),
267            InteractionResult::Success
268        );
269        assert!(player.fishing_hook().is_some());
270    }
271}