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) {
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 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 nbt.insert("Item", self.get_item().to_nbt_tag());
282 }
283
284 fn load_additional(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
285 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}