Skip to main content

steel_core/behavior/items/
ender_eye.rs

1//! Ender eye item behavior implementation.
2
3use std::sync::Arc;
4
5use crate::entity::Entity;
6use crate::worldgen::generator::ChunkGenerator;
7use steel_macros::item_behavior;
8use steel_protocol::packets::game::SoundSource;
9use steel_registry::blocks::block_state_ext::BlockStateExt;
10use steel_registry::blocks::properties::{BlockStateProperties, Direction};
11use steel_registry::item_stack::ItemStack;
12use steel_registry::stat::vanilla_stat_types;
13use steel_registry::{
14    REGISTRY, level_events, sound_events, vanilla_blocks, vanilla_entities, vanilla_game_events,
15    vanilla_structure_tags,
16};
17use steel_registry::{TaggedRegistryExt, vanilla_items};
18use steel_utils::{BlockPos, types::UpdateFlags};
19
20use crate::behavior::ItemBehavior;
21use crate::behavior::block::push_entities_up;
22use crate::behavior::context::{InteractionResult, UseOnContext};
23use crate::behavior::item_utils::get_player_pov_hit_result;
24use crate::entity::entities::EyeOfEnderEntity;
25use crate::entity::{SharedEntity, next_entity_id};
26use crate::world::game_event::GameEventContext;
27use crate::world::{ClipFluid, LevelReader, World};
28
29use glam::DVec3;
30
31const END_PORTAL_PATTERN_DISTANCE: i32 = 5;
32const END_PORTAL_PATTERN: [[char; 5]; 5] = [
33    ['?', 'v', 'v', 'v', '?'],
34    ['>', '?', '?', '?', '<'],
35    ['>', '?', '?', '?', '<'],
36    ['>', '?', '?', '?', '<'],
37    ['?', '^', '^', '^', '?'],
38];
39const PATTERN_DIRECTIONS: [Direction; 6] = [
40    Direction::Down,
41    Direction::Up,
42    Direction::North,
43    Direction::South,
44    Direction::West,
45    Direction::East,
46];
47
48/// Behavior for the ender eye item.
49///
50/// When used on an end portal frame without an eye, places the eye
51/// and checks for portal completion.
52#[item_behavior]
53pub struct EnderEyeItem;
54
55impl ItemBehavior for EnderEyeItem {
56    fn use_on(&self, context: &mut UseOnContext) -> InteractionResult {
57        let clicked_pos = context.hit_result.block_pos;
58        let clicked_state = context.world.get_block_state(clicked_pos);
59
60        let Some(clicked_block) = REGISTRY.blocks.by_state_id(clicked_state) else {
61            return InteractionResult::Pass;
62        };
63
64        if clicked_block.key != vanilla_blocks::END_PORTAL_FRAME.key {
65            return InteractionResult::Pass;
66        }
67
68        let has_eye: bool = clicked_state.get_value(&BlockStateProperties::EYE);
69        if has_eye {
70            return InteractionResult::Pass;
71        }
72
73        let new_state = clicked_state.set_value(&BlockStateProperties::EYE, true);
74        let new_state = push_entities_up(clicked_state, new_state, context.world, clicked_pos);
75
76        if !context
77            .world
78            .set_block(clicked_pos, new_state, UpdateFlags::UPDATE_CLIENTS)
79        {
80            return InteractionResult::Pass;
81        }
82        context
83            .world
84            .update_neighbor_for_output_signal(clicked_pos, &vanilla_blocks::END_PORTAL_FRAME);
85
86        // Play the end portal frame fill sound effect (no exclusion, all players hear it)
87        context
88            .world
89            .level_event(level_events::END_PORTAL_FRAME_FILL, clicked_pos, 0, None);
90
91        context.inv.with_item(ItemStack::shrink_one);
92
93        if let Some(portal_origin) = find_completed_end_portal_origin(context.world, clicked_pos) {
94            spawn_end_portal(context.world, portal_origin);
95        }
96
97        InteractionResult::Success
98    }
99
100    fn use_item(&self, context: &mut crate::behavior::UseItemContext) -> InteractionResult {
101        let world = context.world;
102
103        let hit = get_player_pov_hit_result(world, context.player, ClipFluid::None);
104        if !hit.miss
105            && let Some(hit_block) = REGISTRY
106                .blocks
107                .by_state_id(world.get_block_state(hit.block_pos))
108            && hit_block.key == vanilla_blocks::END_PORTAL_FRAME.key
109        {
110            return InteractionResult::Pass;
111        }
112
113        let Some(structure_generator) = world
114            .chunk_map
115            .world_gen_context
116            .generator
117            .structure_generator()
118        else {
119            log::warn!("World generator not found");
120            return InteractionResult::Consume;
121        };
122
123        let Some(structures) = REGISTRY
124            .structures
125            .get_tag(&vanilla_structure_tags::StructureTag::EYE_OF_ENDER_LOCATED)
126        else {
127            log::debug!("Can't find `EYE_OF_ENDER_LOCATED` tag");
128            return InteractionResult::Consume;
129        };
130
131        let structure_keys = structures
132            .iter()
133            .map(|structure| structure.key.clone())
134            .collect::<Vec<_>>();
135
136        let Some(structure_locate_plan) =
137            structure_generator.locate_plan_for_structures(&structure_keys)
138        else {
139            log::debug!("No structure found");
140            return InteractionResult::Consume;
141        };
142
143        let strongholds = structure_locate_plan.ring_candidates(context.player.block_position());
144
145        let Some(closest_stronghold) = strongholds.first() else {
146            return InteractionResult::Consume;
147        };
148
149        let stronghold_pos = closest_stronghold.locate_pos;
150
151        let player_pos = context.player.position();
152        let spawn_pos = DVec3::new(
153            player_pos.x,
154            player_pos.y + f64::from(context.player.base().dimensions().height * 0.5),
155            player_pos.z,
156        );
157
158        let target_pos = DVec3::new(
159            f64::from(stronghold_pos.x()),
160            f64::from(stronghold_pos.y()),
161            f64::from(stronghold_pos.z()),
162        );
163
164        world.game_event_at(
165            &vanilla_game_events::PROJECTILE_SHOOT,
166            spawn_pos,
167            &GameEventContext::new(Some(context.player), None),
168        );
169
170        let pitch = 0.4 / rand::random_range(0.8..1.2);
171        world.play_sound_at(
172            &sound_events::ENTITY_ENDER_EYE_LAUNCH,
173            SoundSource::Neutral,
174            player_pos,
175            1.0,
176            pitch,
177            None,
178        );
179
180        let thrown_stack = context.inv.with_item(|stack| stack.copy_with_count(1));
181        let eye = EyeOfEnderEntity::with_item(
182            &vanilla_entities::EYE_OF_ENDER,
183            next_entity_id(),
184            spawn_pos,
185            thrown_stack,
186            Arc::downgrade(world),
187        );
188
189        eye.init_target_pos(target_pos);
190
191        let entity: SharedEntity = Arc::new(eye);
192        if let Err(error) = world.try_add_entity(entity) {
193            log::debug!("failed to spawn eye of ender: {error}");
194            return InteractionResult::Consume;
195        }
196
197        let has_infinite_materials = context.player.has_infinite_materials();
198        context
199            .inv
200            .with_item(|item| item.consume_one(has_infinite_materials));
201        context
202            .player
203            .award_stat(&vanilla_stat_types::ITEM_USED, &vanilla_items::ENDER_EYE);
204
205        InteractionResult::SuccessServer
206
207        // TODO implement advancment
208    }
209}
210
211fn find_completed_end_portal_origin(
212    level: &impl LevelReader,
213    clicked_pos: BlockPos,
214) -> Option<BlockPos> {
215    for z in clicked_pos.z()..clicked_pos.z() + END_PORTAL_PATTERN_DISTANCE {
216        for y in clicked_pos.y()..clicked_pos.y() + END_PORTAL_PATTERN_DISTANCE {
217            for x in clicked_pos.x()..clicked_pos.x() + END_PORTAL_PATTERN_DISTANCE {
218                let front_top_left = BlockPos::new(x, y, z);
219                for forwards in PATTERN_DIRECTIONS {
220                    for up in PATTERN_DIRECTIONS {
221                        if up == forwards || up == forwards.opposite() {
222                            continue;
223                        }
224                        if end_portal_pattern_matches(level, front_top_left, forwards, up) {
225                            return Some(front_top_left.offset(-3, 0, -3));
226                        }
227                    }
228                }
229            }
230        }
231    }
232
233    None
234}
235
236fn end_portal_pattern_matches(
237    level: &impl LevelReader,
238    front_top_left: BlockPos,
239    forwards: Direction,
240    up: Direction,
241) -> bool {
242    let forwards_vector = forwards.offset_vec();
243    let up_vector = up.offset_vec();
244    let right_vector = forwards_vector.cross(up_vector);
245
246    for right in 0..5 {
247        for down in 0..5 {
248            let pattern_pos = BlockPos(front_top_left.0 + up_vector * -down + right_vector * right);
249            if !end_portal_pattern_entry_matches(
250                level,
251                pattern_pos,
252                END_PORTAL_PATTERN[down as usize][right as usize],
253            ) {
254                return false;
255            }
256        }
257    }
258
259    true
260}
261
262fn end_portal_pattern_entry_matches(
263    level: &impl LevelReader,
264    pos: BlockPos,
265    pattern_entry: char,
266) -> bool {
267    match pattern_entry {
268        '?' => true,
269        '^' => end_portal_frame_matches(level, pos, Direction::South),
270        '>' => end_portal_frame_matches(level, pos, Direction::West),
271        'v' => end_portal_frame_matches(level, pos, Direction::North),
272        '<' => end_portal_frame_matches(level, pos, Direction::East),
273        _ => false,
274    }
275}
276
277fn end_portal_frame_matches(level: &impl LevelReader, pos: BlockPos, facing: Direction) -> bool {
278    let state = level.get_block_state(pos);
279    state.get_block() == &vanilla_blocks::END_PORTAL_FRAME
280        && state.get_value(&BlockStateProperties::EYE)
281        && state.get_value(&BlockStateProperties::HORIZONTAL_FACING) == facing
282}
283
284fn spawn_end_portal(world: &Arc<World>, portal_origin: BlockPos) {
285    let portal_state = vanilla_blocks::END_PORTAL.default_state();
286    for x_offset in 0..3 {
287        for z_offset in 0..3 {
288            let portal_pos = portal_origin.offset(x_offset, 0, z_offset);
289            let _ = world.destroy_block(portal_pos, true);
290            let _ = world.set_block(portal_pos, portal_state, UpdateFlags::UPDATE_CLIENTS);
291        }
292    }
293
294    world.global_level_event(
295        level_events::SOUND_END_PORTAL_SPAWN,
296        portal_origin.offset(1, 0, 1),
297        0,
298    );
299}
300
301#[cfg(test)]
302mod tests {
303    use steel_registry::blocks::block_state_ext::BlockStateExt;
304    use steel_registry::blocks::properties::{BlockStateProperties, Direction};
305    use steel_registry::{init_vanilla_registry, vanilla_blocks};
306    use steel_utils::{BlockPos, BlockStateId};
307
308    use crate::test_support::TestLevel;
309
310    use super::find_completed_end_portal_origin;
311
312    fn eye_frame(facing: Direction) -> BlockStateId {
313        vanilla_blocks::END_PORTAL_FRAME
314            .default_state()
315            .set_value(&BlockStateProperties::HORIZONTAL_FACING, facing)
316            .set_value(&BlockStateProperties::EYE, true)
317    }
318
319    fn place_inward_frame_ring(level: &TestLevel, origin: BlockPos) {
320        for offset in 0..3 {
321            level.set_test_block(origin.offset(offset, 0, -1), eye_frame(Direction::South));
322            level.set_test_block(origin.offset(offset, 0, 3), eye_frame(Direction::North));
323            level.set_test_block(origin.offset(-1, 0, offset), eye_frame(Direction::East));
324            level.set_test_block(origin.offset(3, 0, offset), eye_frame(Direction::West));
325        }
326    }
327
328    #[test]
329    fn end_portal_pattern_matches_player_built_inward_layout() {
330        init_vanilla_registry();
331
332        let level = TestLevel::default();
333        let origin = BlockPos::new(4, 64, 9);
334        place_inward_frame_ring(&level, origin);
335
336        assert_eq!(
337            find_completed_end_portal_origin(&level, origin.offset(1, 0, -1)),
338            Some(origin)
339        );
340        assert_eq!(
341            find_completed_end_portal_origin(&level, origin.offset(-1, 0, 2)),
342            Some(origin)
343        );
344        assert_eq!(
345            find_completed_end_portal_origin(&level, origin.offset(2, 0, 3)),
346            Some(origin)
347        );
348        assert_eq!(
349            find_completed_end_portal_origin(&level, origin.offset(3, 0, 0)),
350            Some(origin)
351        );
352    }
353
354    #[test]
355    fn end_portal_pattern_rejects_wrong_side_facing() {
356        init_vanilla_registry();
357
358        let level = TestLevel::default();
359        let origin = BlockPos::new(4, 64, 9);
360        place_inward_frame_ring(&level, origin);
361        level.set_test_block(origin.offset(-1, 0, 1), eye_frame(Direction::West));
362
363        assert_eq!(find_completed_end_portal_origin(&level, origin), None);
364    }
365
366    #[test]
367    fn end_portal_pattern_uses_vanilla_front_top_left_offset() {
368        init_vanilla_registry();
369
370        let level = TestLevel::default();
371        let origin = BlockPos::new(4, 64, 9);
372        place_inward_frame_ring(&level, origin);
373        for offset in 0..3 {
374            level.set_test_block(origin.offset(offset, 0, -1), eye_frame(Direction::North));
375            level.set_test_block(origin.offset(offset, 0, 3), eye_frame(Direction::South));
376        }
377
378        assert_eq!(
379            find_completed_end_portal_origin(&level, origin.offset(1, 0, -1)),
380            Some(origin.offset(0, 0, -4))
381        );
382    }
383}