steel_core/entity/entities/objects/projectiles/
eye_of_ender.rs1use 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#[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
78unsafe impl DowncastType for EyeOfEnderEntity {
80 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/eye_of_ender");
81}
82
83impl EyeOfEnderEntity {
84 #[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 #[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 #[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 fn default_item() -> ItemStack {
130 ItemStack::new(&vanilla_items::ENDER_EYE)
131 }
132
133 #[must_use]
135 pub fn get_item(&self) -> ItemStack {
136 self.entity_data.lock().item_stack.get().clone()
137 }
138
139 pub fn set_item(&self, item: ItemStack) {
141 self.entity_data.lock().item_stack.set(item);
142 }
143
144 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 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 nbt.insert("Item", self.get_item().to_nbt_tag());
280 }
281
282 fn load_additional(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
283 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}