steel_core/command/builtins/
enchant.rs1use 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}