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::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#[item_behavior]
18pub struct FishingRodItem;
19
20impl FishingRodItem {
21 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}