Skip to main content

steel_core/entity/entities/objects/explosives/
end_crystal.rs

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