1use super::{
2 BlockStateExt, DamageSourceInfo, DyeColor, EntityEquipmentRef, EntityRef, EntityRefFlags,
3 Identifier, ItemStack, LootContext, LootContextEntity, NumberProvider, NumberProviderRange,
4 REGISTRY, RegistryExt, RngExt, TaggedRegistryExt,
5};
6
7#[derive(Debug, Clone)]
9pub struct PropertyCheck {
10 pub name: &'static str,
11 pub value: &'static str,
12}
13
14#[derive(Debug, Clone)]
16#[expect(clippy::large_enum_variant)]
17pub enum LootCondition {
18 SurvivesExplosion,
21 BlockStateProperty {
23 block: Identifier,
24 properties: &'static [PropertyCheck],
25 },
26 RandomChance(f32),
28 RandomChanceWithEnchantedBonus {
31 enchantment: Identifier,
32 unenchanted_chance: f32,
33 enchanted_chance: EnchantedChance,
35 },
36 MatchTool(ToolPredicate),
38 TableBonus {
41 enchantment: Identifier,
42 chances: &'static [f32],
43 },
44 Inverted(&'static LootCondition),
46 AnyOf(&'static [LootCondition]),
48 AllOf(&'static [LootCondition]),
50 KilledByPlayer,
52 EntityProperties {
54 entity: LootContextEntity,
55 predicate: EntityPredicate,
56 },
57 DamageSourceProperties { predicate: DamageSourcePredicate },
59 LocationCheck {
61 offset_x: i32,
62 offset_y: i32,
63 offset_z: i32,
64 predicate: LocationPredicate,
65 },
66 WeatherCheck {
68 raining: Option<bool>,
69 thundering: Option<bool>,
70 },
71 TimeCheck {
73 value: NumberProviderRange,
74 period: Option<i64>,
75 },
76 ValueCheck {
78 value: NumberProvider,
79 range: NumberProviderRange,
80 },
81 EnchantmentActiveCheck {
83 enchantment: Identifier,
84 active: bool,
85 },
86 EntityScores {
88 entity: LootContextEntity,
89 scores: &'static [(&'static str, NumberProviderRange)],
90 },
91 Reference(Identifier),
93}
94
95#[derive(Debug, Clone, Copy)]
97pub enum EnchantedChance {
98 Constant(f32),
100 Linear {
102 base: f32,
103 per_level_above_first: f32,
104 },
105}
106
107#[derive(Debug, Clone)]
109pub enum ToolPredicate {
110 Item(Identifier),
112 HasEnchantment {
114 enchantment: Identifier,
115 min_level: i32,
116 },
117 Tag(Identifier),
119 Any,
121}
122
123#[derive(Debug, Clone)]
125pub struct LocationPredicate {
126 pub block: Option<BlockPredicate>,
127}
128
129#[derive(Debug, Clone)]
131pub struct BlockPredicate {
132 pub blocks: Option<Identifier>,
133 pub state: &'static [(&'static str, &'static str)],
134}
135
136#[derive(Debug, Clone)]
138pub struct EntityPredicate {
139 pub entity_type: Option<Identifier>,
140 pub flags: Option<EntityFlags>,
141 pub equipment: Option<EntityEquipment>,
142 pub sheep_color: Option<DyeColor>,
144 pub sheep_sheared: Option<bool>,
146 pub chicken_variant: Option<Identifier>,
148}
149
150#[derive(Debug, Clone)]
152pub struct EntityFlags {
153 pub is_on_fire: Option<bool>,
154 pub is_sneaking: Option<bool>,
155 pub is_sprinting: Option<bool>,
156 pub is_swimming: Option<bool>,
157 pub is_baby: Option<bool>,
158}
159
160#[derive(Debug, Clone)]
162pub struct EntityEquipment {
163 pub mainhand: Option<ToolPredicate>,
164 pub offhand: Option<ToolPredicate>,
165 pub head: Option<ToolPredicate>,
166 pub chest: Option<ToolPredicate>,
167 pub legs: Option<ToolPredicate>,
168 pub feet: Option<ToolPredicate>,
169}
170
171#[derive(Debug, Clone)]
173pub struct DamageSourcePredicate {
174 pub tags: &'static [DamageTagPredicate],
176 pub source_entity: Option<EntityPredicate>,
178 pub direct_entity: Option<EntityPredicate>,
180 pub is_direct: Option<bool>,
182}
183
184#[derive(Debug, Clone)]
186pub struct DamageTagPredicate {
187 pub id: Identifier,
188 pub expected: bool,
189}
190
191impl LootCondition {
192 pub fn test<R: rand::Rng>(&self, ctx: &mut LootContext<'_, R>) -> bool {
194 match self {
195 LootCondition::SurvivesExplosion => {
196 if let Some(radius) = ctx.explosion_radius {
197 ctx.rng.random::<f32>() <= (1.0 / radius)
199 } else {
200 true }
202 }
203 LootCondition::BlockStateProperty { block, properties } => {
204 if let Some(state) = ctx.block_state {
205 let state_block = state.get_block();
206 if state_block.key != *block {
208 return false;
209 }
210 for prop in *properties {
212 if let Some(value) = state.get_property_str(prop.name) {
213 if value != prop.value {
214 return false;
215 }
216 } else {
217 return false; }
219 }
220 true
221 } else {
222 false }
224 }
225 LootCondition::RandomChance(chance) => ctx.rng.random::<f32>() < *chance,
226 LootCondition::RandomChanceWithEnchantedBonus {
227 enchantment,
228 unenchanted_chance,
229 enchanted_chance,
230 } => {
231 let level = ctx.get_enchantment_level_by_id(enchantment);
232 let effective_chance = if level > 0 {
233 match enchanted_chance {
234 EnchantedChance::Constant(c) => *c,
235 EnchantedChance::Linear {
236 base,
237 per_level_above_first,
238 } => base + per_level_above_first * (level - 1) as f32,
239 }
240 } else {
241 *unenchanted_chance
242 };
243 ctx.rng.random::<f32>() < effective_chance
244 }
245 LootCondition::MatchTool(predicate) => {
246 if let Some(tool) = ctx.tool {
247 predicate.test(tool, ctx)
248 } else {
249 matches!(predicate, ToolPredicate::Any)
251 }
252 }
253 LootCondition::TableBonus {
254 enchantment,
255 chances,
256 } => {
257 let level = ctx.get_enchantment_level_by_id(enchantment);
258 let index = (level as usize).min(chances.len().saturating_sub(1));
259 let chance = chances.get(index).copied().unwrap_or(0.0);
260 ctx.rng.random::<f32>() < chance
261 }
262 LootCondition::Inverted(inner) => !inner.test(ctx),
263 LootCondition::AnyOf(conditions) => conditions.iter().any(|c| c.test(ctx)),
264 LootCondition::AllOf(conditions) => conditions.iter().all(|c| c.test(ctx)),
265 LootCondition::KilledByPlayer => ctx.killed_by_player,
266 LootCondition::EntityProperties { entity, predicate } => {
267 let Some(entity) = ctx.get_entity(*entity) else {
268 return false;
269 };
270 predicate.test(entity, ctx)
271 }
272 LootCondition::DamageSourceProperties { predicate } => predicate.test(ctx),
273 LootCondition::LocationCheck { .. } => {
274 true
276 }
277 LootCondition::WeatherCheck {
278 raining,
279 thundering,
280 } => {
281 let weather = ctx.weather.unwrap_or_default();
282 raining.is_none_or(|r| r == weather.raining)
283 && thundering.is_none_or(|t| t == weather.thundering)
284 }
285 LootCondition::TimeCheck { value, period } => {
286 let game_time = ctx.game_time.unwrap_or(0);
287 let time = if let Some(p) = period {
288 game_time % p
289 } else {
290 game_time
291 };
292 value.test(time as f32, ctx.rng)
293 }
294 LootCondition::ValueCheck { value, range } => {
295 let v = value.get_simple(ctx.rng);
296 range.test(v, ctx.rng)
297 }
298 LootCondition::EnchantmentActiveCheck {
299 enchantment,
300 active,
301 } => {
302 let level = ctx.get_enchantment_level_by_id(enchantment);
303 let is_active = level > 0;
304 is_active == *active
305 }
306 LootCondition::EntityScores { .. } => {
307 true
309 }
310 LootCondition::Reference(_name) => {
311 true
314 }
315 }
316 }
317}
318
319impl ToolPredicate {
320 #[must_use]
322 pub fn test<R: rand::Rng>(&self, tool: &ItemStack, _ctx: &LootContext<'_, R>) -> bool {
323 match self {
324 ToolPredicate::Item(item_id) => tool.item.key == *item_id,
325 ToolPredicate::HasEnchantment {
326 enchantment,
327 min_level,
328 } => tool_enchantment_matches(tool, enchantment, *min_level),
329 ToolPredicate::Tag(tag) => {
330 REGISTRY.items.is_in_tag(tool.item, tag)
332 }
333 ToolPredicate::Any => true,
334 }
335 }
336}
337
338fn tool_enchantment_matches(
339 tool: &ItemStack,
340 enchantment_or_tag: &Identifier,
341 min_level: i32,
342) -> bool {
343 if tool.get_enchantment_level(enchantment_or_tag) >= min_level {
344 return true;
345 }
346
347 let Some(enchantments) = tool.get_enchantments() else {
348 return false;
349 };
350
351 for (key, level) in enchantments.iter() {
352 if *level < min_level as u32 {
353 continue;
354 }
355 let Some(enchantment) = REGISTRY.enchantments.by_key(key) else {
356 continue;
357 };
358 if REGISTRY
359 .enchantments
360 .is_in_tag(enchantment, enchantment_or_tag)
361 {
362 return true;
363 }
364 }
365
366 false
367}
368
369impl EntityPredicate {
370 fn test<R: rand::Rng>(&self, entity: EntityRef<'_>, ctx: &LootContext<'_, R>) -> bool {
371 if let Some(entity_type) = &self.entity_type
372 && entity.entity_type != Some(entity_type)
373 {
374 return false;
375 }
376
377 if let Some(flags) = &self.flags
378 && !flags.test(entity.flags)
379 {
380 return false;
381 }
382
383 if let Some(equipment) = &self.equipment
384 && !equipment.test(entity.equipment, ctx)
385 {
386 return false;
387 }
388
389 if let Some(expected_color) = &self.sheep_color
390 && entity.sheep_color != Some(*expected_color)
391 {
392 return false;
393 }
394
395 if let Some(expected_sheared) = &self.sheep_sheared
396 && entity.sheep_sheared != Some(*expected_sheared)
397 {
398 return false;
399 }
400
401 if let Some(expected_variant) = &self.chicken_variant
402 && entity.chicken_variant != Some(expected_variant)
403 {
404 return false;
405 }
406
407 true
408 }
409}
410
411impl EntityFlags {
412 fn test(&self, flags: EntityRefFlags) -> bool {
413 self.is_on_fire
414 .is_none_or(|expected| expected == flags.is_on_fire)
415 && self
416 .is_sneaking
417 .is_none_or(|expected| expected == flags.is_sneaking)
418 && self
419 .is_sprinting
420 .is_none_or(|expected| expected == flags.is_sprinting)
421 && self
422 .is_swimming
423 .is_none_or(|expected| expected == flags.is_swimming)
424 && self
425 .is_baby
426 .is_none_or(|expected| expected == flags.is_baby)
427 }
428}
429
430impl EntityEquipment {
431 fn test<R: rand::Rng>(
432 &self,
433 equipment: Option<&EntityEquipmentRef<'_>>,
434 ctx: &LootContext<'_, R>,
435 ) -> bool {
436 let has_predicate = self.mainhand.is_some()
437 || self.offhand.is_some()
438 || self.head.is_some()
439 || self.chest.is_some()
440 || self.legs.is_some()
441 || self.feet.is_some();
442 if !has_predicate {
443 return true;
444 }
445
446 let Some(equipment) = equipment else {
447 return false;
448 };
449
450 slot_predicate_matches(&self.mainhand, equipment.mainhand, ctx)
451 && slot_predicate_matches(&self.offhand, equipment.offhand, ctx)
452 && slot_predicate_matches(&self.head, equipment.head, ctx)
453 && slot_predicate_matches(&self.chest, equipment.chest, ctx)
454 && slot_predicate_matches(&self.legs, equipment.legs, ctx)
455 && slot_predicate_matches(&self.feet, equipment.feet, ctx)
456 }
457}
458
459fn slot_predicate_matches<R: rand::Rng>(
460 predicate: &Option<ToolPredicate>,
461 item_stack: Option<&ItemStack>,
462 ctx: &LootContext<'_, R>,
463) -> bool {
464 let Some(predicate) = predicate else {
465 return true;
466 };
467 let Some(item_stack) = item_stack else {
468 return false;
469 };
470 predicate.test(item_stack, ctx)
471}
472
473impl DamageSourcePredicate {
474 fn test<R: rand::Rng>(&self, ctx: &LootContext<'_, R>) -> bool {
475 let Some(damage_source) = ctx.damage_source else {
476 return false;
477 };
478
479 for tag in self.tags {
480 if damage_source_has_tag(damage_source, &tag.id) != tag.expected {
481 return false;
482 }
483 }
484
485 if let Some(expected) = self.is_direct
486 && damage_source.is_direct != expected
487 {
488 return false;
489 }
490
491 if let Some(predicate) = &self.source_entity {
492 let Some(entity) = ctx.killer_entity else {
493 return false;
494 };
495 if !predicate.test(entity, ctx) {
496 return false;
497 }
498 }
499
500 if let Some(predicate) = &self.direct_entity {
501 let Some(entity) = ctx.direct_killer_entity else {
502 return false;
503 };
504 if !predicate.test(entity, ctx) {
505 return false;
506 }
507 }
508
509 true
510 }
511}
512
513fn damage_source_has_tag(damage_source: DamageSourceInfo<'_>, tag: &Identifier) -> bool {
514 if let Some(damage_type) = damage_source.damage_type
515 && let Some(damage_type) = REGISTRY.damage_types.by_key(damage_type)
516 {
517 return REGISTRY.damage_types.is_in_tag(damage_type, tag);
518 }
519
520 damage_source.tags.iter().any(|candidate| candidate == tag)
521}