Skip to main content

steel_core/entity/entities/
end_crystal.rs

1//! Minimal End Crystal entity implementation for End spike worldgen.
2
3use std::sync::Weak;
4
5use glam::DVec3;
6use simdnbt::borrow::NbtCompound as BorrowedNbtCompoundView;
7use simdnbt::owned::{NbtCompound, NbtTag};
8use steel_macros::entity_behavior;
9use steel_registry::entity_type::EntityTypeRef;
10use steel_registry::vanilla_entity_data::EndCrystalEntityData;
11use steel_utils::{BlockPos, locks::SyncMutex};
12use steel_utils::{DowncastType, DowncastTypeKey};
13
14use crate::entity::{Entity, EntityBase, EntityBaseLoad, EntitySyncedData};
15use crate::world::World;
16
17/// End Crystal entity state needed by worldgen and persistence.
18///
19/// Steel currently implements the synchronized data and saved fields used by generated
20/// End spikes. Portal handling, dragon fight callbacks, and explosion behavior are still
21/// intentionally left to the broader entity/combat foundations.
22#[entity_behavior(class = "EndCrystal")]
23pub struct EndCrystalEntity {
24    base: EntityBase,
25    entity_type: EntityTypeRef,
26    entity_data: SyncMutex<EndCrystalEntityData>,
27}
28
29// SAFETY: This key is owned by Steel and uniquely identifies `EndCrystalEntity`.
30unsafe impl DowncastType for EndCrystalEntity {
31    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/end_crystal");
32}
33
34impl EndCrystalEntity {
35    /// Creates a new End Crystal entity.
36    #[must_use]
37    pub fn new(entity_type: EntityTypeRef, id: i32, position: DVec3, world: Weak<World>) -> Self {
38        Self {
39            base: EntityBase::new(id, position, entity_type.dimensions, world),
40            entity_type,
41            entity_data: SyncMutex::new(EndCrystalEntityData::new()),
42        }
43    }
44
45    /// Creates an End Crystal entity from saved data.
46    #[must_use]
47    pub fn from_saved(entity_type: EntityTypeRef, load: EntityBaseLoad) -> Self {
48        Self {
49            base: EntityBase::from_load(load, entity_type.dimensions),
50            entity_type,
51            entity_data: SyncMutex::new(EndCrystalEntityData::new()),
52        }
53    }
54
55    /// Sets the optional beam target.
56    pub fn set_beam_target(&self, target: Option<BlockPos>) {
57        self.entity_data.lock().beam_target.set(target);
58    }
59
60    /// Returns the optional beam target.
61    #[must_use]
62    pub fn beam_target(&self) -> Option<BlockPos> {
63        *self.entity_data.lock().beam_target.get()
64    }
65
66    /// Sets whether the crystal renders its bedrock base.
67    pub fn set_show_bottom(&self, show_bottom: bool) {
68        self.entity_data.lock().show_bottom.set(show_bottom);
69    }
70
71    /// Returns whether the crystal renders its bedrock base.
72    #[must_use]
73    pub fn shows_bottom(&self) -> bool {
74        *self.entity_data.lock().show_bottom.get()
75    }
76
77    /// Sets position and rotation, matching vanilla `Entity.snapTo`.
78    ///
79    /// # Panics
80    ///
81    /// Panics if the active world entity manager rejects the snap position. This is an invariant
82    /// failure for loaded end crystals.
83    pub fn snap_to(&self, position: DVec3, yaw: f32, pitch: f32) {
84        if let Err(error) = self.base.try_set_position(position) {
85            panic!(
86                "failed to commit end crystal {} snap position: {error}",
87                self.base.id()
88            );
89        }
90        self.base.set_rotation((yaw, pitch));
91        self.set_old_position_to_current();
92    }
93
94    const fn nbt_bool(value: bool) -> i8 {
95        if value { 1 } else { 0 }
96    }
97}
98
99impl Entity for EndCrystalEntity {
100    fn base(&self) -> &EntityBase {
101        &self.base
102    }
103
104    fn entity_type(&self) -> EntityTypeRef {
105        self.entity_type
106    }
107
108    fn tick(&self) {
109        // TODO: Implement portal handling, fire refresh, dragon fight callbacks, and explosion behavior.
110    }
111
112    fn is_pickable(&self) -> bool {
113        true
114    }
115
116    fn blocks_building(&self) -> bool {
117        true
118    }
119
120    fn synced_data(&self) -> Option<&dyn EntitySyncedData> {
121        Some(&self.entity_data)
122    }
123
124    fn save_additional(&self, nbt: &mut NbtCompound) {
125        if let Some(target) = self.beam_target() {
126            nbt.insert(
127                "beam_target",
128                NbtTag::IntArray(vec![target.x(), target.y(), target.z()]),
129            );
130        }
131
132        nbt.insert("ShowBottom", Self::nbt_bool(self.shows_bottom()));
133    }
134
135    fn load_additional(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
136        if let Some(target) = nbt.int_array("beam_target")
137            && target.len() == 3
138        {
139            self.set_beam_target(Some(BlockPos::new(target[0], target[1], target[2])));
140        }
141
142        if let Some(show_bottom) = nbt.byte("ShowBottom") {
143            self.set_show_bottom(show_bottom != 0);
144        }
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    use steel_registry::vanilla_entities;
153
154    #[test]
155    fn end_crystal_does_not_duplicate_shared_invulnerable_state() {
156        let crystal = EndCrystalEntity::new(
157            &vanilla_entities::END_CRYSTAL,
158            1,
159            DVec3::new(1.5, 2.5, 3.5),
160            Weak::new(),
161        );
162        crystal.set_invulnerable(true);
163
164        let mut nbt = NbtCompound::new();
165        crystal.save_additional(&mut nbt);
166
167        assert_eq!(nbt.byte("Invulnerable"), None);
168    }
169
170    #[test]
171    fn end_crystal_is_pickable_like_vanilla() {
172        let crystal = EndCrystalEntity::new(
173            &vanilla_entities::END_CRYSTAL,
174            1,
175            DVec3::new(1.5, 2.5, 3.5),
176            Weak::new(),
177        );
178
179        assert!(crystal.is_pickable());
180    }
181
182    #[test]
183    fn end_crystal_blocks_building_like_vanilla() {
184        let crystal =
185            EndCrystalEntity::new(&vanilla_entities::END_CRYSTAL, 1, DVec3::ZERO, Weak::new());
186
187        assert!(crystal.blocks_building());
188    }
189}