Skip to main content

steel_core/command/builtins/
enchant.rs

1//! Vanilla main-hand enchantment command.
2
3use std::borrow::Cow;
4
5use steel_registry::{
6    REGISTRY, TaggedRegistryExt as _,
7    enchantment::{Enchantment, EnchantmentRef},
8    equipment::EquipmentSlot,
9    vanilla_enchantment_tags::EnchantmentTag,
10};
11use steel_utils::{Identifier, translations};
12use text_components::{Modifier, TextComponent, format::Color, translation::TranslatedMessage};
13
14use super::super::{
15    brigadier::{ArgumentType, CommandNodeBuilder, CommandSyntaxError},
16    execution::{
17        CommandSource, SteelArgumentType, SteelCommandContext, SteelCommandRuntime, argument,
18        literal,
19    },
20    registration::CommandRegistration,
21};
22use crate::entity::{LivingEntity, SharedEntity};
23
24pub(super) fn registration() -> CommandRegistration<CommandSource> {
25    CommandRegistration::new(Identifier::vanilla_static("enchant"), |_| command())
26}
27
28fn command() -> CommandNodeBuilder<CommandSource, SteelCommandRuntime> {
29    literal("enchant").then(
30        argument("targets", SteelArgumentType::entities()).then(
31            argument("enchantment", SteelArgumentType::enchantment())
32                .executes(enchant_default_level)
33                .then(
34                    argument("level", ArgumentType::integer(0, i32::MAX))
35                        .executes(enchant_with_level),
36                ),
37        ),
38    )
39}
40
41fn enchant_default_level(
42    context: &SteelCommandContext<CommandSource>,
43) -> Result<i32, CommandSyntaxError> {
44    enchant(context, 1)
45}
46
47fn enchant_with_level(
48    context: &SteelCommandContext<CommandSource>,
49) -> Result<i32, CommandSyntaxError> {
50    let level = context.integer("level")?;
51    enchant(context, level)
52}
53
54fn enchant(
55    context: &SteelCommandContext<CommandSource>,
56    level: i32,
57) -> Result<i32, CommandSyntaxError> {
58    let targets = context.entities("targets")?;
59    let enchantment = context.enchantment("enchantment")?;
60    let level = u32::try_from(level)
61        .map_err(|_| CommandSyntaxError::dynamic("Enchantment level cannot be negative"))?;
62    if level > enchantment.max_level {
63        let message = translations::COMMANDS_ENCHANT_FAILED_LEVEL
64            .message([level.to_string(), enchantment.max_level.to_string()])
65            .component();
66        return Err(CommandSyntaxError::dynamic(message));
67    }
68
69    let mut success = 0usize;
70    for target in &targets {
71        let Some(living) = target.as_living_entity() else {
72            if targets.len() == 1 {
73                return Err(not_living_error(target));
74            }
75            continue;
76        };
77
78        match enchant_main_hand(living, enchantment, level) {
79            EnchantTargetResult::Enchanted => success += 1,
80            EnchantTargetResult::Itemless if targets.len() == 1 => {
81                return Err(itemless_error(target));
82            }
83            EnchantTargetResult::Incompatible(item_name) if targets.len() == 1 => {
84                return Err(incompatible_error(item_name));
85            }
86            EnchantTargetResult::Itemless | EnchantTargetResult::Incompatible(_) => {}
87        }
88    }
89
90    if success == 0 {
91        return Err(CommandSyntaxError::dynamic(TextComponent::from(
92            &translations::COMMANDS_ENCHANT_FAILED,
93        )));
94    }
95
96    let enchantment_name = enchantment_display_name(enchantment, level);
97    let message = if let [target] = targets.as_slice() {
98        translations::COMMANDS_ENCHANT_SUCCESS_SINGLE
99            .message([
100                enchantment_name,
101                TextComponent::plain(target.plain_text_name()),
102            ])
103            .component()
104    } else {
105        translations::COMMANDS_ENCHANT_SUCCESS_MULTIPLE
106            .message([
107                enchantment_name,
108                TextComponent::from(targets.len().to_string()),
109            ])
110            .component()
111    };
112    context.source().send_success(&message, true);
113
114    i32::try_from(success).map_err(|_| {
115        CommandSyntaxError::dynamic("Enchanted entity count exceeds the command result range")
116    })
117}
118
119#[derive(Debug, PartialEq, Eq)]
120enum EnchantTargetResult {
121    Enchanted,
122    Itemless,
123    Incompatible(Box<str>),
124}
125
126fn enchant_main_hand(
127    target: &dyn LivingEntity,
128    enchantment: EnchantmentRef,
129    level: u32,
130) -> EnchantTargetResult {
131    let mut result = EnchantTargetResult::Itemless;
132    target.with_equipment_slot(EquipmentSlot::MainHand, &mut |item| {
133        if item.is_empty() {
134            return;
135        }
136        if !enchantment.can_enchant(item.item())
137            || !Enchantment::is_compatible_with_existing(enchantment, item)
138        {
139            result =
140                EnchantTargetResult::Incompatible(item.item().key.to_string().into_boxed_str());
141            return;
142        }
143        result = EnchantTargetResult::Enchanted;
144    });
145
146    if result == EnchantTargetResult::Enchanted {
147        target.with_equipment_slot_mut(EquipmentSlot::MainHand, &mut |item| {
148            item.upgrade_enchantment(enchantment.key.clone(), level);
149        });
150    }
151    result
152}
153
154fn not_living_error(target: &SharedEntity) -> CommandSyntaxError {
155    let message = translations::COMMANDS_ENCHANT_FAILED_ENTITY
156        .message([TextComponent::plain(target.plain_text_name())])
157        .component();
158    CommandSyntaxError::dynamic(message)
159}
160
161fn itemless_error(target: &SharedEntity) -> CommandSyntaxError {
162    let message = translations::COMMANDS_ENCHANT_FAILED_ITEMLESS
163        .message([TextComponent::plain(target.plain_text_name())])
164        .component();
165    CommandSyntaxError::dynamic(message)
166}
167
168fn incompatible_error(item_name: Box<str>) -> CommandSyntaxError {
169    let message = translations::COMMANDS_ENCHANT_FAILED_INCOMPATIBLE
170        .message([TextComponent::plain(String::from(item_name))])
171        .component();
172    CommandSyntaxError::dynamic(message)
173}
174
175fn enchantment_display_name(enchantment: EnchantmentRef, level: u32) -> TextComponent {
176    let color = if REGISTRY
177        .enchantments
178        .is_in_tag(enchantment, &EnchantmentTag::CURSE)
179    {
180        Color::Red
181    } else {
182        Color::Gray
183    };
184    let mut component = TextComponent::translated(TranslatedMessage {
185        key: Cow::Owned(format!(
186            "enchantment.{}.{}",
187            enchantment.key.namespace, enchantment.key.path
188        )),
189        args: None,
190        fallback: None,
191    })
192    .color(color);
193
194    if level != 1 || enchantment.max_level != 1 {
195        component =
196            component
197                .add_child(TextComponent::plain(" "))
198                .add_child(TextComponent::translated(TranslatedMessage {
199                    key: Cow::Owned(format!("enchantment.level.{level}")),
200                    args: None,
201                    fallback: None,
202                }));
203    }
204    component
205}
206
207#[cfg(test)]
208mod tests {
209    use std::sync::Weak;
210
211    use glam::DVec3;
212    use steel_registry::{
213        entity_type::EntityTypeRef, equipment::EquipmentSlot, init_vanilla_registry,
214        item_stack::ItemStack, vanilla_enchantments, vanilla_entities, vanilla_items,
215    };
216    use steel_utils::locks::SyncMutex;
217
218    use super::super::create_dispatcher;
219    use super::{EnchantTargetResult, enchant_main_hand};
220    use crate::{
221        command::{
222            brigadier::{ArgumentType, CommandDispatcher, NodeId},
223            execution::{CommandSource, SteelArgumentType, SteelCommandRuntime},
224        },
225        entity::{Entity, EntityBase, LivingEntity, LivingEntityBase},
226    };
227
228    type Dispatcher = CommandDispatcher<CommandSource, SteelCommandRuntime>;
229
230    fn child(dispatcher: &Dispatcher, parent: NodeId, name: &str) -> NodeId {
231        let Some(children) = dispatcher.children(parent) else {
232            panic!("parent node should exist");
233        };
234        let Some(child) = children.iter().copied().find(|child| {
235            dispatcher
236                .node(*child)
237                .is_some_and(|node| node.name() == name)
238        }) else {
239            panic!("child {name} should exist");
240        };
241        child
242    }
243
244    #[test]
245    fn enchant_graph_uses_all_entities_and_an_enchantment_resource() {
246        init_vanilla_registry();
247        let Ok(dispatcher) = create_dispatcher() else {
248            panic!("built-in commands should register");
249        };
250        let enchant = child(&dispatcher, dispatcher.root(), "enchant");
251        let targets = child(&dispatcher, enchant, "targets");
252        assert_eq!(
253            dispatcher
254                .node(targets)
255                .and_then(|node| node.argument_type()),
256            Some(&SteelArgumentType::entities())
257        );
258
259        let enchantment = child(&dispatcher, targets, "enchantment");
260        assert_eq!(
261            dispatcher
262                .node(enchantment)
263                .and_then(|node| node.argument_type()),
264            Some(&SteelArgumentType::enchantment())
265        );
266        assert!(matches!(
267            dispatcher.node(enchantment),
268            Some(node) if node.is_executable()
269        ));
270
271        let level = child(&dispatcher, enchantment, "level");
272        assert_eq!(
273            dispatcher.node(level).and_then(|node| node.argument_type()),
274            Some(&SteelArgumentType::from(ArgumentType::integer(0, i32::MAX)))
275        );
276    }
277
278    #[test]
279    fn enchant_main_hand_applies_once_and_then_rejects_the_same_enchantment() {
280        init_vanilla_registry();
281        let target = TestLivingEntity::new(&vanilla_entities::ZOMBIE);
282        target.equip(ItemStack::new(&vanilla_items::DIAMOND_SWORD));
283
284        assert_eq!(
285            enchant_main_hand(&target, &vanilla_enchantments::SHARPNESS, 1),
286            EnchantTargetResult::Enchanted
287        );
288        assert_eq!(target.main_hand_enchantment_level(), 1);
289        assert!(matches!(
290            enchant_main_hand(&target, &vanilla_enchantments::SHARPNESS, 2),
291            EnchantTargetResult::Incompatible(_)
292        ));
293        assert_eq!(target.main_hand_enchantment_level(), 1);
294    }
295
296    struct TestLivingEntity {
297        base: EntityBase,
298        living_base: LivingEntityBase,
299        health: SyncMutex<f32>,
300        entity_type: EntityTypeRef,
301    }
302
303    impl TestLivingEntity {
304        fn new(entity_type: EntityTypeRef) -> Self {
305            Self {
306                base: EntityBase::new(1, DVec3::ZERO, entity_type.dimensions, Weak::new()),
307                living_base: LivingEntityBase::new(entity_type),
308                health: SyncMutex::new(20.0),
309                entity_type,
310            }
311        }
312
313        fn equip(&self, stack: ItemStack) {
314            self.living_base
315                .equipment()
316                .lock()
317                .set(EquipmentSlot::MainHand, stack);
318        }
319
320        fn main_hand_enchantment_level(&self) -> i32 {
321            let mut level = 0;
322            self.with_equipment_slot(EquipmentSlot::MainHand, &mut |item| {
323                level = item.get_enchantment_level(&vanilla_enchantments::SHARPNESS.key);
324            });
325            level
326        }
327    }
328
329    crate::entity::impl_test_downcast_type!(TestLivingEntity);
330
331    impl Entity for TestLivingEntity {
332        fn base(&self) -> &EntityBase {
333            &self.base
334        }
335
336        fn entity_type(&self) -> EntityTypeRef {
337            self.entity_type
338        }
339    }
340
341    impl LivingEntity for TestLivingEntity {
342        fn living_base(&self) -> &LivingEntityBase {
343            &self.living_base
344        }
345
346        fn get_health(&self) -> f32 {
347            *self.health.lock()
348        }
349
350        fn set_health(&self, health: f32) {
351            *self.health.lock() = health;
352        }
353
354        fn get_absorption_amount(&self) -> f32 {
355            0.0
356        }
357
358        fn set_absorption_amount(&self, _amount: f32) {}
359    }
360}