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::enchantment_effect::EnchantmentEffectComponent;
10use steel_registry::sound_events::{ENTITY_FISHING_BOBBER_RETRIEVE, ENTITY_FISHING_BOBBER_THROW};
11use steel_registry::stat::vanilla_stat_types;
12use steel_registry::{vanilla_entities, vanilla_items};
13
14/// Behavior for the fishing rod item.
15#[item_behavior]
16pub struct FishingRodItem;
17
18impl ItemBehavior for FishingRodItem {
19    fn use_item(&self, context: &mut UseItemContext) -> InteractionResult {
20        let player = context.player;
21        let infinite_materials = context.player.has_infinite_materials();
22        let world = context.world;
23        let item = context.inv.with_item(|item| item.clone());
24
25        let pitch = 0.4 / rng().random_range(0.8..1.2);
26
27        if let Some(fishing) = player.fishing_hook() {
28            if item.is(&vanilla_items::FISHING_ROD) {
29                let damage = fishing.retrieve(&item);
30
31                context.inv.with_item(|item| {
32                    item.hurt_and_break(damage, infinite_materials);
33                });
34            }
35
36            world.play_sound_at(
37                &ENTITY_FISHING_BOBBER_RETRIEVE,
38                SoundSource::Neutral,
39                player.position(),
40                1.0,
41                pitch,
42                None,
43            );
44            // TODO: add vibration (Java equivalent: `ItemStack.causeVibration()`)
45            // Problem: `ItemStack` in steel currently doesn't implement any fn that elicits this behaviour
46            // Possibility: Re-Use the bone-meal exclusive implementation the bone-meal PR introduced
47            //
48            // Another Problem: If we were to do that, we'd run into a circular dependency; the existing implementation for bone-meal uses `UseItemOnContext`
49            // (better would be `UseItemContext`, if we were to re-use this for all `ItemStacks`), which resides inside `steel-core`, but `ItemStack` resides inside
50            // `steel-registry` and `steel-core` depends on `steel-registry` (I think, lmao)
51        } else {
52            world.play_sound_at(
53                &ENTITY_FISHING_BOBBER_THROW,
54                SoundSource::Neutral,
55                player.position(),
56                0.5,
57                pitch,
58                None,
59            );
60
61            let player_pos = player.position();
62            let spawn_pos = DVec3::new(player_pos.x, player.get_eye_y() - 0.1, player_pos.z);
63
64            let luck = item.apply_unconditional_enchantment_value_effects(
65                EnchantmentEffectComponent::FishingLuckBonus,
66                0.0,
67            );
68
69            let lure_speed = item.apply_unconditional_enchantment_value_effects(
70                EnchantmentEffectComponent::FishingTimeReduction,
71                0.0,
72            );
73
74            let hook = Arc::new(FishingHookEntity::new(
75                &vanilla_entities::FISHING_BOBBER,
76                next_entity_id(),
77                spawn_pos,
78                Arc::downgrade(world),
79            ));
80
81            let player_arc = world
82                .players
83                .get_by_uuid(&player.gameprofile.id)
84                .expect("Failed to obtain `player_arc` for launching fishing hook: `player` must be registered in the world.");
85
86            hook.shoot_from_player(&player_arc, luck as i32, lure_speed as i32);
87
88            let entity: SharedEntity = hook;
89
90            if let Err(error) = world.try_add_entity(Arc::clone(&entity)) {
91                entity.set_removed(RemovalReason::Discarded);
92                log::error!("Failed to spawn fishing hook: {error}");
93                return InteractionResult::Fail;
94            }
95
96            player.award_stat(&vanilla_stat_types::ITEM_USED, item.item());
97
98            // TODO: add vibration, see above TODO
99        }
100        InteractionResult::Success
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use steel_registry::{item_stack::ItemStack, vanilla_items};
107    use steel_utils::types::InteractionHand;
108    use uuid::Uuid;
109
110    use super::*;
111    use crate::behavior::UseItemContext;
112    use crate::test_support::{TestPlayerBuilder, fresh_test_world};
113
114    #[test]
115    fn retrieving_grounded_hook_does_not_relock_inventory() {
116        let world = fresh_test_world("fishing_rod_grounded_retrieve");
117        let player = TestPlayerBuilder::new(Arc::clone(&world), Uuid::from_u128(1), 1).build();
118        player
119            .inventory
120            .lock()
121            .set_selected_item(ItemStack::new(&vanilla_items::FISHING_ROD));
122
123        let player_owner = Arc::clone(&player);
124        let owner: SharedEntity = player_owner;
125        let hook = Arc::new(FishingHookEntity::new(
126            &vanilla_entities::FISHING_BOBBER,
127            2,
128            DVec3::ZERO,
129            Arc::downgrade(&world),
130        ));
131        hook.set_owner(&owner);
132        hook.set_on_ground(true);
133
134        let mut context = UseItemContext::new(
135            &player,
136            InteractionHand::MainHand,
137            &world,
138            Arc::clone(&player.inventory),
139        );
140
141        assert_eq!(
142            FishingRodItem.use_item(&mut context),
143            InteractionResult::Success
144        );
145        assert!(hook.is_removed());
146        assert!(player.fishing_hook().is_none());
147        assert_eq!(context.inv.with_item(|item| item.get_damage_value()), 2);
148    }
149
150    #[test]
151    fn casting_fishing_rod_spawns_and_links_hook() {
152        use crate::test_support::insert_ready_full_chunk;
153        use steel_utils::ChunkPos;
154
155        let world = fresh_test_world("fishing_rod_cast");
156        insert_ready_full_chunk(&world, ChunkPos::new(0, 0));
157        let player = TestPlayerBuilder::new(Arc::clone(&world), Uuid::from_u128(2), 10).build();
158        player
159            .try_set_position(DVec3::new(8.0, 64.0, 8.0))
160            .expect("should position player in center of chunk");
161        world.players.insert(Arc::clone(&player));
162        player
163            .inventory
164            .lock()
165            .set_selected_item(ItemStack::new(&vanilla_items::FISHING_ROD));
166
167        let mut context = UseItemContext::new(
168            &player,
169            InteractionHand::MainHand,
170            &world,
171            Arc::clone(&player.inventory),
172        );
173
174        assert_eq!(
175            FishingRodItem.use_item(&mut context),
176            InteractionResult::Success
177        );
178        assert!(player.fishing_hook().is_some());
179    }
180}