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::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    const fn nbt_bool(value: bool) -> i8 {
78        if value { 1 } else { 0 }
79    }
80}
81
82impl Entity for EndCrystalEntity {
83    fn base(&self) -> &EntityBase {
84        &self.base
85    }
86
87    fn entity_type(&self) -> EntityTypeRef {
88        self.entity_type
89    }
90
91    fn tick(&self) {
92        // TODO: Implement portal handling, fire refresh, dragon fight callbacks, and explosion behavior.
93    }
94
95    fn is_pickable(&self) -> bool {
96        true
97    }
98
99    fn blocks_building(&self) -> bool {
100        true
101    }
102
103    fn synced_data(&self) -> Option<&dyn EntitySyncedData> {
104        Some(&self.entity_data)
105    }
106
107    fn save_additional(&self, nbt: &mut NbtCompound) {
108        if let Some(target) = self.beam_target() {
109            nbt.insert(
110                "beam_target",
111                NbtTag::IntArray(vec![target.x(), target.y(), target.z()]),
112            );
113        }
114
115        nbt.insert("ShowBottom", Self::nbt_bool(self.shows_bottom()));
116    }
117
118    fn load_additional(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
119        if let Some(target) = nbt.int_array("beam_target")
120            && target.len() == 3
121        {
122            self.set_beam_target(Some(BlockPos::new(target[0], target[1], target[2])));
123        }
124
125        if let Some(show_bottom) = nbt.byte("ShowBottom") {
126            self.set_show_bottom(show_bottom != 0);
127        }
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    use steel_registry::vanilla_entities;
136
137    #[test]
138    fn end_crystal_does_not_duplicate_shared_invulnerable_state() {
139        let crystal = EndCrystalEntity::new(
140            &vanilla_entities::END_CRYSTAL,
141            1,
142            DVec3::new(1.5, 2.5, 3.5),
143            Weak::new(),
144        );
145        crystal.set_invulnerable(true);
146
147        let mut nbt = NbtCompound::new();
148        crystal.save_additional(&mut nbt);
149
150        assert_eq!(nbt.byte("Invulnerable"), None);
151    }
152
153    #[test]
154    fn end_crystal_is_pickable_like_vanilla() {
155        let crystal = EndCrystalEntity::new(
156            &vanilla_entities::END_CRYSTAL,
157            1,
158            DVec3::new(1.5, 2.5, 3.5),
159            Weak::new(),
160        );
161
162        assert!(crystal.is_pickable());
163    }
164
165    #[test]
166    fn end_crystal_blocks_building_like_vanilla() {
167        let crystal =
168            EndCrystalEntity::new(&vanilla_entities::END_CRYSTAL, 1, DVec3::ZERO, Weak::new());
169
170        assert!(crystal.blocks_building());
171    }
172}