Skip to main content

steel_registry/
enchantment.rs

1use crate::items::ItemRef;
2pub use crate::loot_table::EquipmentSlotGroup;
3use crate::{REGISTRY, RegistryEntry, RegistryExt, TaggedRegistryExt};
4use rustc_hash::FxHashMap;
5use simdnbt::ToNbtTag;
6use simdnbt::owned::{NbtCompound, NbtList, NbtTag};
7use steel_utils::Identifier;
8
9/// Enchanting cost formula: `base + per_level_above_first * (level - 1)`.
10#[derive(Debug, Clone, Copy)]
11pub struct EnchantmentCost {
12    pub base: i32,
13    pub per_level_above_first: i32,
14}
15
16#[derive(Debug)]
17pub struct Enchantment {
18    pub key: Identifier,
19    pub max_level: u32,
20    pub min_cost: EnchantmentCost,
21    pub max_cost: EnchantmentCost,
22    pub anvil_cost: i32,
23    pub weight: u32,
24    pub slots: &'static [EquipmentSlotGroup],
25    pub supported_items: &'static str,
26    pub primary_items: Option<&'static str>,
27    pub exclusive_set: Option<&'static str>,
28    // TODO: effects (data-driven, complex nested JSON structures)
29}
30
31impl RegistryEntry for Enchantment {
32    fn key(&self) -> &Identifier {
33        &self.key
34    }
35
36    fn try_id(&self) -> Option<usize> {
37        REGISTRY.enchantments.id_from_key(&self.key)
38    }
39}
40
41impl ToNbtTag for &Enchantment {
42    fn to_nbt_tag(self) -> NbtTag {
43        let mut compound = NbtCompound::new();
44
45        // description: translatable text component {"translate": "enchantment.minecraft.<key>"}
46        let mut desc = NbtCompound::new();
47        desc.insert(
48            "translate",
49            format!("enchantment.{}.{}", self.key.namespace, self.key.path).as_str(),
50        );
51        compound.insert("description", NbtTag::Compound(desc));
52
53        // Definition fields (inlined, not nested)
54        compound.insert("supported_items", self.supported_items);
55        if let Some(primary) = self.primary_items {
56            compound.insert("primary_items", primary);
57        }
58        compound.insert("weight", self.weight as i32);
59        compound.insert("max_level", self.max_level as i32);
60
61        let mut min_cost = NbtCompound::new();
62        min_cost.insert("base", self.min_cost.base);
63        min_cost.insert("per_level_above_first", self.min_cost.per_level_above_first);
64        compound.insert("min_cost", NbtTag::Compound(min_cost));
65
66        let mut max_cost = NbtCompound::new();
67        max_cost.insert("base", self.max_cost.base);
68        max_cost.insert("per_level_above_first", self.max_cost.per_level_above_first);
69        compound.insert("max_cost", NbtTag::Compound(max_cost));
70
71        compound.insert("anvil_cost", self.anvil_cost);
72
73        let slots: Vec<String> = self.slots.iter().map(|s| s.as_str().to_owned()).collect();
74        compound.insert("slots", NbtTag::List(NbtList::from(slots)));
75
76        if let Some(exclusive) = self.exclusive_set {
77            compound.insert("exclusive_set", exclusive);
78        }
79
80        // TODO: effects (data-driven, complex nested JSON structures)
81
82        NbtTag::Compound(compound)
83    }
84}
85
86/// Parses a tag reference string like `"#minecraft:foo"` into an `Identifier`.
87fn parse_tag_ref(tag_ref: &str) -> Option<Identifier> {
88    let without_hash = tag_ref.strip_prefix('#')?;
89    Some(if let Some((ns, path)) = without_hash.split_once(':') {
90        Identifier::new(ns.to_owned(), path.to_owned())
91    } else {
92        Identifier::vanilla(without_hash.to_owned())
93    })
94}
95
96impl Enchantment {
97    /// Checks if this enchantment can be applied to the given item via `supported_items` tag.
98    pub fn can_enchant(&self, item: ItemRef) -> bool {
99        let Some(tag) = parse_tag_ref(self.supported_items) else {
100            return false;
101        };
102        REGISTRY.items.is_in_tag(item, &tag)
103    }
104
105    /// Checks if two enchantments are compatible (neither's `exclusive_set` contains the other).
106    pub fn are_compatible(a: EnchantmentRef, b: EnchantmentRef) -> bool {
107        if a == b {
108            return false;
109        }
110        if let Some(set) = a.exclusive_set
111            && let Some(tag) = parse_tag_ref(set)
112            && REGISTRY.enchantments.is_in_tag(b, &tag)
113        {
114            return false;
115        }
116        if let Some(set) = b.exclusive_set
117            && let Some(tag) = parse_tag_ref(set)
118            && REGISTRY.enchantments.is_in_tag(a, &tag)
119        {
120            return false;
121        }
122        true
123    }
124
125    /// Checks if this enchantment is compatible with all existing enchantments on an item.
126    pub fn is_compatible_with_existing(
127        enchantment: EnchantmentRef,
128        item: &crate::item_stack::ItemStack,
129    ) -> bool {
130        let Some(enchantments) = item.get_enchantments() else {
131            return true;
132        };
133        for (existing_key, _) in enchantments.iter() {
134            if *existing_key == enchantment.key {
135                continue;
136            }
137            let Some(existing) = REGISTRY.enchantments.by_key(existing_key) else {
138                continue;
139            };
140            if !Self::are_compatible(enchantment, existing) {
141                return false;
142            }
143        }
144        true
145    }
146}
147
148pub type EnchantmentRef = &'static Enchantment;
149
150impl PartialEq for EnchantmentRef {
151    #[expect(clippy::disallowed_methods)] // This IS the PartialEq impl; ptr::eq is correct here
152    fn eq(&self, other: &Self) -> bool {
153        std::ptr::eq(*self, *other)
154    }
155}
156
157impl Eq for EnchantmentRef {}
158
159pub struct EnchantmentRegistry {
160    enchantments_by_id: Vec<EnchantmentRef>,
161    enchantments_by_key: FxHashMap<Identifier, usize>,
162    tags: FxHashMap<Identifier, Vec<Identifier>>,
163    allows_registering: bool,
164}
165
166impl EnchantmentRegistry {
167    #[must_use]
168    pub fn new() -> Self {
169        Self {
170            enchantments_by_id: Vec::new(),
171            enchantments_by_key: FxHashMap::default(),
172            tags: FxHashMap::default(),
173            allows_registering: true,
174        }
175    }
176}
177
178crate::impl_registry_ext!(
179    EnchantmentRegistry,
180    Enchantment,
181    enchantments_by_id,
182    enchantments_by_key
183);
184
185crate::impl_standard_methods!(
186    EnchantmentRegistry,
187    EnchantmentRef,
188    enchantments_by_id,
189    enchantments_by_key,
190    allows_registering
191);
192
193crate::impl_tagged_registry!(EnchantmentRegistry, enchantments_by_key, "enchantment");