Skip to main content

steel_core/entity/entities/objects/projectiles/
eye_of_ender.rs

1//! Eye of ender entity implementation (`EyeOfEnderEntity`).
2//!
3//! Thrown by [`EnderEyeItem`](crate::behavior::items::EnderEyeItem) to point
4//! toward the nearest stronghold. Unlike its neighbors in this module, vanilla's
5//! `EyeOfEnderEntity` extends plain `Entity` (not `Projectile`/`ThrowableProjectile`)
6//! and drives its own position/velocity manually toward a stored target each tick,
7//! rather than using inherited projectile physics.
8
9use std::sync::{Arc, Weak};
10
11use glam::DVec3;
12use steel_macros::entity_behavior;
13use steel_math::lerp;
14use steel_registry::entity_type::EntityTypeRef;
15use steel_registry::item_stack::ItemStack;
16use steel_registry::vanilla_entity_data::EyeOfEnderEntityData;
17use steel_registry::{level_events, sound_events, vanilla_entities, vanilla_items};
18use steel_utils::locks::SyncMutex;
19use steel_utils::{BlockPos, DowncastType, DowncastTypeKey};
20
21use simdnbt::ToNbtTag;
22use simdnbt::borrow::NbtCompound as BorrowedNbtCompoundView;
23use simdnbt::owned::NbtCompound;
24
25use crate::entity::entities::ItemEntity;
26use crate::entity::{
27    Entity, EntityBase, EntityBaseLoad, EntityBaseState, EntitySyncedData, RemovalReason,
28    SharedEntity, next_entity_id,
29};
30use crate::world::World;
31
32const LIFESPAN_TICKS: u32 = 80;
33
34const TOO_FAR_DISTANCE: f64 = 12.0;
35
36const TOO_FAR_SIGNAL_HEIGHT: f64 = 8.0;
37
38const VELOCITY_LERP_ALPHA: f64 = 0.0025;
39
40const NEAR_TARGET_THRESHOLD: f64 = 1.0;
41
42const NEAR_TARGET_DAMPING: f64 = 0.8;
43
44const VERTICAL_NUDGE: f64 = 0.015;
45
46struct EyeOfEnderState {
47    target_pos: Option<DVec3>,
48
49    lifespan: u32,
50
51    drops_item: bool,
52}
53
54impl EyeOfEnderState {
55    const fn new() -> Self {
56        Self {
57            target_pos: None,
58            lifespan: 0,
59            drops_item: false,
60        }
61    }
62}
63
64/// A thrown eye of ender, seeking the nearest stronghold.
65///
66/// Mirrors vanilla's `EyeOfEnderEntity`:
67/// - Flies toward a target point set by `init_target_pos`
68/// - Despawns after 80 ticks, dropping itself (4/5 chance) or shattering
69/// - Not attackable
70#[entity_behavior(class = "EyeOfEnder")]
71pub struct EyeOfEnderEntity {
72    base: EntityBase,
73    entity_type: EntityTypeRef,
74    entity_data: SyncMutex<EyeOfEnderEntityData>,
75    state: SyncMutex<EyeOfEnderState>,
76}
77
78// SAFETY: This key is owned by Steel and uniquely identifies `EyeOfEnderEntity`.
79unsafe impl DowncastType for EyeOfEnderEntity {
80    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/eye_of_ender");
81}
82
83impl EyeOfEnderEntity {
84    /// Creates a new eye of ender with the default (plain ender eye) displayed item.
85    #[must_use]
86    pub fn new(entity_type: EntityTypeRef, id: i32, position: DVec3, world: Weak<World>) -> Self {
87        Self::with_item(entity_type, id, position, Self::default_item(), world)
88    }
89
90    /// Creates a new eye of ender with the specified displayed item.
91    #[must_use]
92    pub fn with_item(
93        entity_type: EntityTypeRef,
94        id: i32,
95        position: DVec3,
96        item: ItemStack,
97        world: Weak<World>,
98    ) -> Self {
99        let mut entity_data = EyeOfEnderEntityData::new();
100        entity_data.item_stack.set(item);
101
102        Self {
103            base: EntityBase::new_with_state(
104                id,
105                EntityBaseState::new(position, entity_type.dimensions),
106                world,
107            ),
108            entity_type,
109            entity_data: SyncMutex::new(entity_data),
110            state: SyncMutex::new(EyeOfEnderState::new()),
111        }
112    }
113
114    /// Creates an eye of ender from saved data with restored base state.
115    #[must_use]
116    pub fn from_saved(entity_type: EntityTypeRef, load: EntityBaseLoad) -> Self {
117        let mut entity_data = EyeOfEnderEntityData::new();
118        entity_data.item_stack.set(Self::default_item());
119
120        Self {
121            base: EntityBase::from_load(load, entity_type.dimensions),
122            entity_type,
123            entity_data: SyncMutex::new(entity_data),
124            state: SyncMutex::new(EyeOfEnderState::new()),
125        }
126    }
127
128    /// Vanilla `EyeOfEnderEntity.getItem` (private default): a plain ender eye.
129    fn default_item() -> ItemStack {
130        ItemStack::new(&vanilla_items::ENDER_EYE)
131    }
132
133    /// Gets a clone of the displayed item stack.
134    #[must_use]
135    pub fn get_item(&self) -> ItemStack {
136        self.entity_data.lock().item_stack.get().clone()
137    }
138
139    /// Sets the displayed item stack.
140    pub fn set_item(&self, item: ItemStack) {
141        self.entity_data.lock().item_stack.set(item);
142    }
143
144    /// Sets the point this eye flies toward, resets its lifespan, and rerolls
145    /// whether it will drop itself when it expires.
146    ///
147    /// Mirrors vanilla `EyeOfEnderEntity.initTargetPos`.
148    pub fn init_target_pos(&self, pos: DVec3) {
149        let diff = pos - self.position();
150        let horizontal_dist = DVec3::new(diff.x, 0.0, diff.z).length();
151
152        let target = if horizontal_dist > TOO_FAR_DISTANCE {
153            self.position()
154                + DVec3::new(
155                    diff.x / horizontal_dist * TOO_FAR_DISTANCE,
156                    TOO_FAR_SIGNAL_HEIGHT,
157                    diff.z / horizontal_dist * TOO_FAR_DISTANCE,
158                )
159        } else {
160            pos
161        };
162
163        let mut state = self.state.lock();
164        state.target_pos = Some(target);
165        state.lifespan = 0;
166        state.drops_item = rand::random_range(0..5) > 0;
167    }
168
169    /// Vanilla `EyeOfEnderEntity.updateVelocity` (static).
170    ///
171    /// Deliberate divergence: vanilla computes `lv.multiply(e / d)` without
172    /// guarding `d`, so an eye sitting exactly on its target's X/Z divides by
173    /// zero and poisons its own position with `NaN`. Steel asserts positions are
174    /// finite, which would turn that into a world-tick panic, so the horizontal
175    /// term is skipped when there is no horizontal distance left to cover. The
176    /// vertical nudge still applies, matching what vanilla does for every
177    /// non-degenerate input.
178    fn update_velocity(velocity: DVec3, current_pos: DVec3, target_pos: DVec3) -> DVec3 {
179        let horizontal = DVec3::new(
180            target_pos.x - current_pos.x,
181            0.0,
182            target_pos.z - current_pos.z,
183        );
184        let d = horizontal.length();
185        let mut e = lerp(
186            VELOCITY_LERP_ALPHA,
187            DVec3::new(velocity.x, 0.0, velocity.z).length(),
188            d,
189        );
190        let mut f = velocity.y;
191        if d < NEAR_TARGET_THRESHOLD {
192            e *= NEAR_TARGET_DAMPING;
193            f *= NEAR_TARGET_DAMPING;
194        }
195        let g = if current_pos.y - velocity.y < target_pos.y {
196            1.0
197        } else {
198            -1.0
199        };
200
201        let vertical = DVec3::new(0.0, f + (g - f) * VERTICAL_NUDGE, 0.0);
202        if d == 0.0 {
203            return vertical;
204        }
205
206        horizontal * (e / d) + vertical
207    }
208}
209
210impl Entity for EyeOfEnderEntity {
211    fn base(&self) -> &EntityBase {
212        &self.base
213    }
214
215    fn entity_type(&self) -> EntityTypeRef {
216        self.entity_type
217    }
218
219    fn synced_data(&self) -> Option<&dyn EntitySyncedData> {
220        Some(&self.entity_data)
221    }
222
223    fn attackable(&self) -> bool {
224        false
225    }
226
227    fn tick(&self) {
228        let next_pos = self.position() + self.velocity();
229
230        let target_pos = self.state.lock().target_pos;
231        if let Some(target_pos) = target_pos {
232            self.set_velocity(Self::update_velocity(self.velocity(), next_pos, target_pos));
233        }
234
235        if self.base().try_set_position(next_pos).is_err() {
236            self.set_removed(RemovalReason::Discarded);
237            return;
238        }
239
240        let (lifespan, drops_item) = {
241            let mut state = self.state.lock();
242            state.lifespan += 1;
243            (state.lifespan, state.drops_item)
244        };
245
246        if lifespan <= LIFESPAN_TICKS {
247            return;
248        }
249
250        self.play_sound(&sound_events::ENTITY_ENDER_EYE_DEATH, 1.0, 1.0);
251        self.set_removed(RemovalReason::Discarded);
252
253        let Some(world) = self.level() else {
254            return;
255        };
256
257        if drops_item {
258            let item = ItemEntity::with_item(
259                &vanilla_entities::ITEM,
260                next_entity_id(),
261                self.position(),
262                self.get_item(),
263                Arc::downgrade(&world),
264            );
265            let entity: SharedEntity = Arc::new(item);
266            if let Err(error) = world.try_add_entity(entity) {
267                log::warn!("failed to drop eye of ender item: {error}");
268            }
269        } else {
270            world.level_event(
271                level_events::PARTICLES_EYE_OF_ENDER_DEATH,
272                BlockPos::from(self.position()),
273                0,
274                None,
275            );
276        }
277    }
278
279    fn save_additional(&self, nbt: &mut NbtCompound) {
280        // Mirrors vanilla `EyeOfEnderEntity.writeCustomData`: only the displayed item persists.
281        nbt.insert("Item", self.get_item().to_nbt_tag());
282    }
283
284    fn load_additional(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
285        // Mirrors vanilla `EyeOfEnderEntity.readCustomData`: falls back to the
286        // current item (a plain ender eye by default) if absent/unreadable.
287        if let Some(item_tag) = nbt.compound("Item")
288            && let Some(item) = ItemStack::from_borrowed_compound(&item_tag)
289        {
290            self.set_item(item);
291        }
292    }
293}