steel_core/behavior/items/
fishing_rod.rs1use 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#[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 } 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 }
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}