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    pub fn init_target_pos(&self, pos: DVec3) {
147        let diff = pos - self.position();
148        let horizontal_dist = DVec3::new(diff.x, 0.0, diff.z).length();
149
150        let target = if horizontal_dist > TOO_FAR_DISTANCE {
151            self.position()
152                + DVec3::new(
153                    diff.x / horizontal_dist * TOO_FAR_DISTANCE,
154                    TOO_FAR_SIGNAL_HEIGHT,
155                    diff.z / horizontal_dist * TOO_FAR_DISTANCE,
156                )
157        } else {
158            pos
159        };
160
161        let mut state = self.state.lock();
162        state.target_pos = Some(target);
163        state.lifespan = 0;
164        state.drops_item = rand::random_range(0..5) > 0;
165    }
166
167    /// Vanilla `EyeOfEnderEntity.updateVelocity` (static).
168    ///
169    /// Deliberate divergence: vanilla computes `lv.multiply(e / d)` without
170    /// guarding `d`, so an eye sitting exactly on its target's X/Z divides by
171    /// zero and poisons its own position with `NaN`. Steel asserts positions are
172    /// finite, which would turn that into a world-tick panic, so the horizontal
173    /// term is skipped when there is no horizontal distance left to cover. The
174    /// vertical nudge still applies, matching what vanilla does for every
175    /// non-degenerate input.
176    fn update_velocity(velocity: DVec3, current_pos: DVec3, target_pos: DVec3) -> DVec3 {
177        let horizontal = DVec3::new(
178            target_pos.x - current_pos.x,
179            0.0,
180            target_pos.z - current_pos.z,
181        );
182        let d = horizontal.length();
183        let mut e = lerp(
184            VELOCITY_LERP_ALPHA,
185            DVec3::new(velocity.x, 0.0, velocity.z).length(),
186            d,
187        );
188        let mut f = velocity.y;
189        if d < NEAR_TARGET_THRESHOLD {
190            e *= NEAR_TARGET_DAMPING;
191            f *= NEAR_TARGET_DAMPING;
192        }
193        let g = if current_pos.y - velocity.y < target_pos.y {
194            1.0
195        } else {
196            -1.0
197        };
198
199        let vertical = DVec3::new(0.0, f + (g - f) * VERTICAL_NUDGE, 0.0);
200        if d == 0.0 {
201            return vertical;
202        }
203
204        horizontal * (e / d) + vertical
205    }
206}
207
208impl Entity for EyeOfEnderEntity {
209    fn base(&self) -> &EntityBase {
210        &self.base
211    }
212
213    fn entity_type(&self) -> EntityTypeRef {
214        self.entity_type
215    }
216
217    fn synced_data(&self) -> Option<&dyn EntitySyncedData> {
218        Some(&self.entity_data)
219    }
220
221    fn attackable(&self) -> bool {
222        false
223    }
224
225    fn tick(self: Arc<Self>) {
226        let next_pos = self.position() + self.velocity();
227
228        let target_pos = self.state.lock().target_pos;
229        if let Some(target_pos) = target_pos {
230            self.set_velocity(Self::update_velocity(self.velocity(), next_pos, target_pos));
231        }
232
233        if self.base().try_set_position(next_pos).is_err() {
234            self.set_removed(RemovalReason::Discarded);
235            return;
236        }
237
238        let (lifespan, drops_item) = {
239            let mut state = self.state.lock();
240            state.lifespan += 1;
241            (state.lifespan, state.drops_item)
242        };
243
244        if lifespan <= LIFESPAN_TICKS {
245            return;
246        }
247
248        self.play_sound(&sound_events::ENTITY_ENDER_EYE_DEATH, 1.0, 1.0);
249        self.set_removed(RemovalReason::Discarded);
250
251        let Some(world) = self.level() else {
252            return;
253        };
254
255        if drops_item {
256            let item = ItemEntity::with_item(
257                &vanilla_entities::ITEM,
258                next_entity_id(),
259                self.position(),
260                self.get_item(),
261                Arc::downgrade(&world),
262            );
263            let entity: SharedEntity = Arc::new(item);
264            if let Err(error) = world.try_add_entity(entity) {
265                log::warn!("failed to drop eye of ender item: {error}");
266            }
267        } else {
268            world.level_event(
269                level_events::PARTICLES_EYE_OF_ENDER_DEATH,
270                BlockPos::from(self.position()),
271                0,
272                None,
273            );
274        }
275    }
276
277    fn save_additional(&self, nbt: &mut NbtCompound) {
278        // Only the displayed item persists.
279        nbt.insert("Item", self.get_item().to_nbt_tag());
280    }
281
282    fn load_additional(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
283        // Falls back to the current item (a plain ender eye by default) if
284        // the tag is absent or unreadable.
285        if let Some(item_tag) = nbt.compound("Item")
286            && let Some(item) = ItemStack::from_borrowed_compound(&item_tag)
287        {
288            self.set_item(item);
289        }
290    }
291}