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