Skip to main content

steel_core/behavior/items/
ender_eye.rs

1//! Ender eye item behavior implementation.
2
3use std::sync::Arc;
4
5use steel_macros::item_behavior;
6use steel_registry::REGISTRY;
7use steel_registry::blocks::block_state_ext::BlockStateExt;
8use steel_registry::blocks::properties::{BlockStateProperties, Direction};
9use steel_registry::level_events;
10use steel_registry::vanilla_blocks;
11use steel_utils::{BlockPos, types::UpdateFlags};
12
13use crate::behavior::ItemBehavior;
14use crate::behavior::block::push_entities_up;
15use crate::behavior::context::{InteractionResult, UseOnContext};
16use crate::world::{LevelReader, World};
17
18const END_PORTAL_PATTERN_DISTANCE: i32 = 5;
19const END_PORTAL_PATTERN: [[char; 5]; 5] = [
20    ['?', 'v', 'v', 'v', '?'],
21    ['>', '?', '?', '?', '<'],
22    ['>', '?', '?', '?', '<'],
23    ['>', '?', '?', '?', '<'],
24    ['?', '^', '^', '^', '?'],
25];
26const PATTERN_DIRECTIONS: [Direction; 6] = [
27    Direction::Down,
28    Direction::Up,
29    Direction::North,
30    Direction::South,
31    Direction::West,
32    Direction::East,
33];
34
35/// Behavior for the ender eye item.
36///
37/// When used on an end portal frame without an eye, places the eye
38/// and checks for portal completion.
39#[item_behavior]
40pub struct EnderEyeItem;
41
42impl ItemBehavior for EnderEyeItem {
43    fn use_on(&self, context: &mut UseOnContext) -> InteractionResult {
44        let clicked_pos = context.hit_result.block_pos;
45        let clicked_state = context.world.get_block_state(clicked_pos);
46
47        let Some(clicked_block) = REGISTRY.blocks.by_state_id(clicked_state) else {
48            return InteractionResult::Pass;
49        };
50
51        if clicked_block.key != vanilla_blocks::END_PORTAL_FRAME.key {
52            return InteractionResult::Pass;
53        }
54
55        let has_eye: bool = clicked_state.get_value(&BlockStateProperties::EYE);
56        if has_eye {
57            return InteractionResult::Pass;
58        }
59
60        let new_state = clicked_state.set_value(&BlockStateProperties::EYE, true);
61        let new_state = push_entities_up(clicked_state, new_state, context.world, clicked_pos);
62
63        if !context
64            .world
65            .set_block(clicked_pos, new_state, UpdateFlags::UPDATE_CLIENTS)
66        {
67            return InteractionResult::Pass;
68        }
69        context
70            .world
71            .update_neighbor_for_output_signal(clicked_pos, &vanilla_blocks::END_PORTAL_FRAME);
72
73        // Play the end portal frame fill sound effect (no exclusion, all players hear it)
74        context
75            .world
76            .level_event(level_events::END_PORTAL_FRAME_FILL, clicked_pos, 0, None);
77
78        context.inv.with_item(|item| item.shrink(1));
79
80        if let Some(portal_origin) = find_completed_end_portal_origin(context.world, clicked_pos) {
81            spawn_end_portal(context.world, portal_origin);
82        }
83
84        InteractionResult::Success
85    }
86}
87
88fn find_completed_end_portal_origin(
89    level: &impl LevelReader,
90    clicked_pos: BlockPos,
91) -> Option<BlockPos> {
92    for z in clicked_pos.z()..clicked_pos.z() + END_PORTAL_PATTERN_DISTANCE {
93        for y in clicked_pos.y()..clicked_pos.y() + END_PORTAL_PATTERN_DISTANCE {
94            for x in clicked_pos.x()..clicked_pos.x() + END_PORTAL_PATTERN_DISTANCE {
95                let front_top_left = BlockPos::new(x, y, z);
96                for forwards in PATTERN_DIRECTIONS {
97                    for up in PATTERN_DIRECTIONS {
98                        if up == forwards || up == forwards.opposite() {
99                            continue;
100                        }
101                        if end_portal_pattern_matches(level, front_top_left, forwards, up) {
102                            return Some(front_top_left.offset(-3, 0, -3));
103                        }
104                    }
105                }
106            }
107        }
108    }
109
110    None
111}
112
113fn end_portal_pattern_matches(
114    level: &impl LevelReader,
115    front_top_left: BlockPos,
116    forwards: Direction,
117    up: Direction,
118) -> bool {
119    let forwards_vector = forwards.offset_vec();
120    let up_vector = up.offset_vec();
121    let right_vector = forwards_vector.cross(up_vector);
122
123    for right in 0..5 {
124        for down in 0..5 {
125            let pattern_pos = BlockPos(front_top_left.0 + up_vector * -down + right_vector * right);
126            if !end_portal_pattern_entry_matches(
127                level,
128                pattern_pos,
129                END_PORTAL_PATTERN[down as usize][right as usize],
130            ) {
131                return false;
132            }
133        }
134    }
135
136    true
137}
138
139fn end_portal_pattern_entry_matches(
140    level: &impl LevelReader,
141    pos: BlockPos,
142    pattern_entry: char,
143) -> bool {
144    match pattern_entry {
145        '?' => true,
146        '^' => end_portal_frame_matches(level, pos, Direction::South),
147        '>' => end_portal_frame_matches(level, pos, Direction::West),
148        'v' => end_portal_frame_matches(level, pos, Direction::North),
149        '<' => end_portal_frame_matches(level, pos, Direction::East),
150        _ => false,
151    }
152}
153
154fn end_portal_frame_matches(level: &impl LevelReader, pos: BlockPos, facing: Direction) -> bool {
155    let state = level.get_block_state(pos);
156    state.get_block() == &vanilla_blocks::END_PORTAL_FRAME
157        && state.get_value(&BlockStateProperties::EYE)
158        && state.get_value(&BlockStateProperties::HORIZONTAL_FACING) == facing
159}
160
161fn spawn_end_portal(world: &Arc<World>, portal_origin: BlockPos) {
162    let portal_state = vanilla_blocks::END_PORTAL.default_state();
163    for x_offset in 0..3 {
164        for z_offset in 0..3 {
165            let portal_pos = portal_origin.offset(x_offset, 0, z_offset);
166            let _ = world.destroy_block(portal_pos, true);
167            let _ = world.set_block(portal_pos, portal_state, UpdateFlags::UPDATE_CLIENTS);
168        }
169    }
170
171    world.global_level_event(
172        level_events::SOUND_END_PORTAL_SPAWN,
173        portal_origin.offset(1, 0, 1),
174        0,
175    );
176}
177
178#[cfg(test)]
179mod tests {
180    use steel_registry::blocks::block_state_ext::BlockStateExt;
181    use steel_registry::blocks::properties::{BlockStateProperties, Direction};
182    use steel_registry::{init_vanilla_registry, vanilla_blocks};
183    use steel_utils::{BlockPos, BlockStateId};
184
185    use crate::test_support::TestLevel;
186
187    use super::find_completed_end_portal_origin;
188
189    fn eye_frame(facing: Direction) -> BlockStateId {
190        vanilla_blocks::END_PORTAL_FRAME
191            .default_state()
192            .set_value(&BlockStateProperties::HORIZONTAL_FACING, facing)
193            .set_value(&BlockStateProperties::EYE, true)
194    }
195
196    fn place_inward_frame_ring(level: &TestLevel, origin: BlockPos) {
197        for offset in 0..3 {
198            level.set_test_block(origin.offset(offset, 0, -1), eye_frame(Direction::South));
199            level.set_test_block(origin.offset(offset, 0, 3), eye_frame(Direction::North));
200            level.set_test_block(origin.offset(-1, 0, offset), eye_frame(Direction::East));
201            level.set_test_block(origin.offset(3, 0, offset), eye_frame(Direction::West));
202        }
203    }
204
205    #[test]
206    fn end_portal_pattern_matches_player_built_inward_layout() {
207        init_vanilla_registry();
208
209        let level = TestLevel::default();
210        let origin = BlockPos::new(4, 64, 9);
211        place_inward_frame_ring(&level, origin);
212
213        assert_eq!(
214            find_completed_end_portal_origin(&level, origin.offset(1, 0, -1)),
215            Some(origin)
216        );
217        assert_eq!(
218            find_completed_end_portal_origin(&level, origin.offset(-1, 0, 2)),
219            Some(origin)
220        );
221        assert_eq!(
222            find_completed_end_portal_origin(&level, origin.offset(2, 0, 3)),
223            Some(origin)
224        );
225        assert_eq!(
226            find_completed_end_portal_origin(&level, origin.offset(3, 0, 0)),
227            Some(origin)
228        );
229    }
230
231    #[test]
232    fn end_portal_pattern_rejects_wrong_side_facing() {
233        init_vanilla_registry();
234
235        let level = TestLevel::default();
236        let origin = BlockPos::new(4, 64, 9);
237        place_inward_frame_ring(&level, origin);
238        level.set_test_block(origin.offset(-1, 0, 1), eye_frame(Direction::West));
239
240        assert_eq!(find_completed_end_portal_origin(&level, origin), None);
241    }
242
243    #[test]
244    fn end_portal_pattern_uses_vanilla_front_top_left_offset() {
245        init_vanilla_registry();
246
247        let level = TestLevel::default();
248        let origin = BlockPos::new(4, 64, 9);
249        place_inward_frame_ring(&level, origin);
250        for offset in 0..3 {
251            level.set_test_block(origin.offset(offset, 0, -1), eye_frame(Direction::North));
252            level.set_test_block(origin.offset(offset, 0, 3), eye_frame(Direction::South));
253        }
254
255        assert_eq!(
256            find_completed_end_portal_origin(&level, origin.offset(1, 0, -1)),
257            Some(origin.offset(0, 0, -4))
258        );
259    }
260}