1use 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#[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 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 }
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}