1use crate::{
2 REGISTRY, RegistryExt, TaggedRegistryExt, blocks::block_state_ext::BlockStateExt,
3 item_stack::ItemStack,
4};
5use rand::RngExt;
6use rustc_hash::FxHashMap;
7use steel_utils::{BlockStateId, Identifier};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum LootContextEntity {
12 This,
14 Killer,
16 DirectKiller,
18 KillerPlayer,
20 Interacting,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum EquipmentSlotGroup {
30 Any,
31 MainHand,
32 OffHand,
33 Hand,
34 Head,
35 Chest,
36 Legs,
37 Feet,
38 Armor,
39 Body,
40}
41
42impl EquipmentSlotGroup {
43 #[must_use]
44 pub const fn as_str(self) -> &'static str {
45 match self {
46 Self::Any => "any",
47 Self::MainHand => "mainhand",
48 Self::OffHand => "offhand",
49 Self::Hand => "hand",
50 Self::Head => "head",
51 Self::Chest => "chest",
52 Self::Legs => "legs",
53 Self::Feet => "feet",
54 Self::Armor => "armor",
55 Self::Body => "body",
56 }
57 }
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum DyeColor {
63 White,
64 Orange,
65 Magenta,
66 LightBlue,
67 Yellow,
68 Lime,
69 Pink,
70 Gray,
71 LightGray,
72 Cyan,
73 Purple,
74 Blue,
75 Brown,
76 Green,
77 Red,
78 Black,
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum LootType {
84 Block,
85 Entity,
86 Chest,
87 Fishing,
88 Gift,
89 Archaeology,
90 Vault,
91 Shearing,
92 Equipment,
93 Selector,
94 EntityInteract,
95 BlockInteract,
96 Barter,
97}
98
99#[derive(Debug, Clone)]
101pub enum NumberProvider {
102 Constant(f32),
103 Uniform {
104 min: f32,
105 max: f32,
106 },
107 Binomial {
108 n: i32,
109 p: f32,
110 },
111 Score {
113 target: ScoreboardTarget,
114 score: &'static str,
115 scale: f32,
116 },
117 Storage {
119 storage: Identifier,
120 path: &'static str,
121 },
122 EnchantmentLevel {
124 enchantment: Identifier,
125 },
126}
127
128#[derive(Debug, Clone, Copy)]
130pub enum ScoreboardTarget {
131 This,
133 Killer,
135 DirectKiller,
137 KillerPlayer,
139 Fixed(&'static str),
141}
142
143impl NumberProvider {
144 pub fn get<R: rand::Rng>(&self, rng: &mut R, ctx: Option<&LootContextRef<'_>>) -> f32 {
146 match self {
147 Self::Constant(v) => *v,
148 Self::Uniform { min, max } => rng.random_range(*min..=*max),
149 Self::Binomial { n, p } => {
150 let mut count = 0;
151 for _ in 0..*n {
152 if rng.random::<f32>() < *p {
153 count += 1;
154 }
155 }
156 count as f32
157 }
158 Self::Score { .. } => {
159 let _ = ctx;
161 0.0
162 }
163 Self::Storage { .. } => {
164 let _ = ctx;
166 0.0
167 }
168 Self::EnchantmentLevel { enchantment } => ctx
169 .and_then(|c| c.tool)
170 .map(|t| t.get_enchantment_level(enchantment) as f32)
171 .unwrap_or(0.0),
172 }
173 }
174
175 pub fn get_simple(&self, rng: &mut impl rand::Rng) -> f32 {
177 match self {
178 Self::Constant(v) => *v,
179 Self::Uniform { min, max } => rng.random_range(*min..=*max),
180 Self::Binomial { n, p } => {
181 let mut count = 0;
182 for _ in 0..*n {
183 if rng.random::<f32>() < *p {
184 count += 1;
185 }
186 }
187 count as f32
188 }
189 Self::Score { .. } | Self::Storage { .. } | Self::EnchantmentLevel { .. } => 0.0,
191 }
192 }
193
194 pub fn get_int(&self, rng: &mut impl rand::Rng) -> i32 {
196 self.get_simple(rng).floor() as i32
197 }
198
199 pub fn get_int_with_ctx<R: rand::Rng>(
201 &self,
202 rng: &mut R,
203 ctx: Option<&LootContextRef<'_>>,
204 ) -> i32 {
205 self.get(rng, ctx).floor() as i32
206 }
207}
208
209#[derive(Debug, Clone)]
211pub struct NumberProviderRange {
212 pub min: Option<NumberProvider>,
213 pub max: Option<NumberProvider>,
214}
215
216impl NumberProviderRange {
217 pub fn test(&self, value: f32, rng: &mut impl rand::Rng) -> bool {
219 if let Some(min) = &self.min
220 && value < min.get_simple(rng)
221 {
222 return false;
223 }
224 if let Some(max) = &self.max
225 && value > max.get_simple(rng)
226 {
227 return false;
228 }
229 true
230 }
231
232 pub const fn exact(value: f32) -> Self {
234 Self {
235 min: Some(NumberProvider::Constant(value)),
236 max: Some(NumberProvider::Constant(value)),
237 }
238 }
239
240 pub const fn at_least(min: f32) -> Self {
242 Self {
243 min: Some(NumberProvider::Constant(min)),
244 max: None,
245 }
246 }
247
248 pub const fn at_most(max: f32) -> Self {
250 Self {
251 min: None,
252 max: Some(NumberProvider::Constant(max)),
253 }
254 }
255
256 pub const fn between(min: f32, max: f32) -> Self {
258 Self {
259 min: Some(NumberProvider::Constant(min)),
260 max: Some(NumberProvider::Constant(max)),
261 }
262 }
263}
264
265pub struct LootContextRef<'a> {
268 pub tool: Option<&'a ItemStack>,
269 }
271
272pub struct LootContext<'a, R: rand::Rng> {
276 pub rng: &'a mut R,
278 pub luck: f32,
280 pub block_state: Option<BlockStateId>,
282 pub tool: Option<&'a ItemStack>,
284 pub explosion_radius: Option<f32>,
286 pub killed_by_player: bool,
288
289 pub origin: Option<(f64, f64, f64)>,
291 pub game_time: Option<i64>,
293 pub weather: Option<WeatherState>,
295 pub this_entity: Option<EntityRef<'a>>,
298 pub killer_entity: Option<EntityRef<'a>>,
300 pub direct_killer_entity: Option<EntityRef<'a>>,
302 pub last_damage_player: Option<EntityRef<'a>>,
304 pub damage_source: Option<DamageSourceInfo<'a>>,
306 pub block_entity: Option<BlockEntityRef<'a>>,
308 pub interacting_entity: Option<EntityRef<'a>>,
310}
311
312#[derive(Debug, Clone, Copy, Default)]
314pub struct WeatherState {
315 pub raining: bool,
316 pub thundering: bool,
317}
318
319#[derive(Debug, Clone, Copy)]
322pub struct EntityRef<'a> {
323 pub entity_type: Option<&'a Identifier>,
325 pub flags: EntityRefFlags,
327 pub equipment: Option<&'a EntityEquipmentRef<'a>>,
329 pub custom_name: Option<&'a str>,
331}
332
333#[derive(Debug, Clone, Copy, Default)]
335pub struct EntityRefFlags {
336 pub is_on_fire: bool,
337 pub is_sneaking: bool,
338 pub is_sprinting: bool,
339 pub is_swimming: bool,
340 pub is_baby: bool,
341}
342
343#[derive(Debug, Clone, Copy)]
345pub struct EntityEquipmentRef<'a> {
346 pub mainhand: Option<&'a ItemStack>,
347 pub offhand: Option<&'a ItemStack>,
348 pub head: Option<&'a ItemStack>,
349 pub chest: Option<&'a ItemStack>,
350 pub legs: Option<&'a ItemStack>,
351 pub feet: Option<&'a ItemStack>,
352}
353
354#[derive(Debug, Clone, Copy)]
356pub struct DamageSourceInfo<'a> {
357 pub damage_type: Option<&'a Identifier>,
359 pub tags: &'a [Identifier],
361 pub is_direct: bool,
363}
364
365#[derive(Debug, Clone, Copy)]
367pub struct BlockEntityRef<'a> {
368 pub block_entity_type: Option<&'a Identifier>,
370 pub custom_name: Option<&'a str>,
372 pub inventory: Option<&'a [ItemStack]>,
374}
375
376impl<'a, R: rand::Rng> LootContext<'a, R> {
377 pub fn new(rng: &'a mut R) -> Self {
379 Self {
380 rng,
381 luck: 0.0,
382 block_state: None,
383 tool: None,
384 explosion_radius: None,
385 killed_by_player: false,
386 origin: None,
387 game_time: None,
388 weather: None,
389 this_entity: None,
390 killer_entity: None,
391 direct_killer_entity: None,
392 last_damage_player: None,
393 damage_source: None,
394 block_entity: None,
395 interacting_entity: None,
396 }
397 }
398
399 pub fn with_luck(mut self, luck: f32) -> Self {
401 self.luck = luck;
402 self
403 }
404
405 pub fn with_block_state(mut self, state: BlockStateId) -> Self {
407 self.block_state = Some(state);
408 self
409 }
410
411 pub fn with_tool(mut self, tool: &'a ItemStack) -> Self {
413 self.tool = Some(tool);
414 self
415 }
416
417 pub fn with_explosion(mut self, radius: f32) -> Self {
419 self.explosion_radius = Some(radius);
420 self
421 }
422
423 pub fn with_killed_by_player(mut self, killed: bool) -> Self {
425 self.killed_by_player = killed;
426 self
427 }
428
429 pub fn with_origin(mut self, x: f64, y: f64, z: f64) -> Self {
431 self.origin = Some((x, y, z));
432 self
433 }
434
435 pub fn with_game_time(mut self, time: i64) -> Self {
437 self.game_time = Some(time);
438 self
439 }
440
441 pub fn with_weather(mut self, weather: WeatherState) -> Self {
443 self.weather = Some(weather);
444 self
445 }
446
447 pub fn with_this_entity(mut self, entity: EntityRef<'a>) -> Self {
449 self.this_entity = Some(entity);
450 self
451 }
452
453 pub fn with_killer_entity(mut self, entity: EntityRef<'a>) -> Self {
455 self.killer_entity = Some(entity);
456 self
457 }
458
459 pub fn with_direct_killer_entity(mut self, entity: EntityRef<'a>) -> Self {
461 self.direct_killer_entity = Some(entity);
462 self
463 }
464
465 pub fn with_last_damage_player(mut self, entity: EntityRef<'a>) -> Self {
467 self.last_damage_player = Some(entity);
468 self
469 }
470
471 pub fn with_damage_source(mut self, damage_source: DamageSourceInfo<'a>) -> Self {
473 self.damage_source = Some(damage_source);
474 self
475 }
476
477 pub fn with_block_entity(mut self, block_entity: BlockEntityRef<'a>) -> Self {
479 self.block_entity = Some(block_entity);
480 self
481 }
482
483 pub fn with_interacting_entity(mut self, entity: EntityRef<'a>) -> Self {
485 self.interacting_entity = Some(entity);
486 self
487 }
488
489 pub fn get_enchantment_level_by_id(&self, enchantment: &Identifier) -> i32 {
491 self.tool
492 .map(|t| t.get_enchantment_level(enchantment))
493 .unwrap_or(0)
494 }
495
496 pub fn get_entity(&self, target: LootContextEntity) -> Option<EntityRef<'a>> {
498 match target {
499 LootContextEntity::This => self.this_entity,
500 LootContextEntity::Killer => self.killer_entity,
501 LootContextEntity::DirectKiller => self.direct_killer_entity,
502 LootContextEntity::KillerPlayer => self.last_damage_player,
503 LootContextEntity::Interacting => self.interacting_entity,
504 }
505 }
506}
507
508#[derive(Debug, Clone)]
510pub struct PropertyCheck {
511 pub name: &'static str,
512 pub value: &'static str,
513}
514
515#[derive(Debug, Clone)]
517#[expect(clippy::large_enum_variant)]
518pub enum LootCondition {
519 SurvivesExplosion,
522 BlockStateProperty {
524 block: Identifier,
525 properties: &'static [PropertyCheck],
526 },
527 RandomChance(f32),
529 RandomChanceWithEnchantedBonus {
532 enchantment: Identifier,
533 unenchanted_chance: f32,
534 enchanted_chance: EnchantedChance,
536 },
537 MatchTool(ToolPredicate),
539 TableBonus {
542 enchantment: Identifier,
543 chances: &'static [f32],
544 },
545 Inverted(&'static LootCondition),
547 AnyOf(&'static [LootCondition]),
549 AllOf(&'static [LootCondition]),
551 KilledByPlayer,
553 EntityProperties {
555 entity: LootContextEntity,
556 predicate: EntityPredicate,
557 },
558 DamageSourceProperties { predicate: DamageSourcePredicate },
560 LocationCheck {
562 offset_x: i32,
563 offset_y: i32,
564 offset_z: i32,
565 predicate: LocationPredicate,
566 },
567 WeatherCheck {
569 raining: Option<bool>,
570 thundering: Option<bool>,
571 },
572 TimeCheck {
574 value: NumberProviderRange,
575 period: Option<i64>,
576 },
577 ValueCheck {
579 value: NumberProvider,
580 range: NumberProviderRange,
581 },
582 EnchantmentActiveCheck {
584 enchantment: Identifier,
585 active: bool,
586 },
587 EntityScores {
589 entity: LootContextEntity,
590 scores: &'static [(&'static str, NumberProviderRange)],
591 },
592 Reference(Identifier),
594}
595
596#[derive(Debug, Clone, Copy)]
598pub enum EnchantedChance {
599 Constant(f32),
601 Linear {
603 base: f32,
604 per_level_above_first: f32,
605 },
606}
607
608#[derive(Debug, Clone)]
610pub enum ToolPredicate {
611 Item(Identifier),
613 HasEnchantment {
615 enchantment: Identifier,
616 min_level: i32,
617 },
618 Tag(Identifier),
620 Any,
622}
623
624#[derive(Debug, Clone)]
626pub struct LocationPredicate {
627 pub block: Option<BlockPredicate>,
628}
629
630#[derive(Debug, Clone)]
632pub struct BlockPredicate {
633 pub blocks: Option<Identifier>,
634 pub state: &'static [(&'static str, &'static str)],
635}
636
637#[derive(Debug, Clone)]
639pub struct EntityPredicate {
640 pub entity_type: Option<Identifier>,
641 pub flags: Option<EntityFlags>,
642 pub equipment: Option<EntityEquipment>,
643}
644
645#[derive(Debug, Clone)]
647pub struct EntityFlags {
648 pub is_on_fire: Option<bool>,
649 pub is_sneaking: Option<bool>,
650 pub is_sprinting: Option<bool>,
651 pub is_swimming: Option<bool>,
652 pub is_baby: Option<bool>,
653}
654
655#[derive(Debug, Clone)]
657pub struct EntityEquipment {
658 pub mainhand: Option<ToolPredicate>,
659 pub offhand: Option<ToolPredicate>,
660 pub head: Option<ToolPredicate>,
661 pub chest: Option<ToolPredicate>,
662 pub legs: Option<ToolPredicate>,
663 pub feet: Option<ToolPredicate>,
664}
665
666#[derive(Debug, Clone)]
668pub struct DamageSourcePredicate {
669 pub tags: &'static [DamageTagPredicate],
671 pub source_entity: Option<EntityPredicate>,
673 pub direct_entity: Option<EntityPredicate>,
675 pub is_direct: Option<bool>,
677}
678
679#[derive(Debug, Clone)]
681pub struct DamageTagPredicate {
682 pub id: Identifier,
683 pub expected: bool,
684}
685
686impl LootCondition {
687 pub fn test<R: rand::Rng>(&self, ctx: &mut LootContext<'_, R>) -> bool {
689 match self {
690 LootCondition::SurvivesExplosion => {
691 if let Some(radius) = ctx.explosion_radius {
692 ctx.rng.random::<f32>() <= (1.0 / radius)
694 } else {
695 true }
697 }
698 LootCondition::BlockStateProperty { block, properties } => {
699 if let Some(state) = ctx.block_state {
700 let state_block = state.get_block();
701 if state_block.key != *block {
703 return false;
704 }
705 for prop in *properties {
707 if let Some(value) = state.get_property_str(prop.name) {
708 if value != prop.value {
709 return false;
710 }
711 } else {
712 return false; }
714 }
715 true
716 } else {
717 false }
719 }
720 LootCondition::RandomChance(chance) => ctx.rng.random::<f32>() < *chance,
721 LootCondition::RandomChanceWithEnchantedBonus {
722 enchantment,
723 unenchanted_chance,
724 enchanted_chance,
725 } => {
726 let level = ctx.get_enchantment_level_by_id(enchantment);
727 let effective_chance = if level > 0 {
728 match enchanted_chance {
729 EnchantedChance::Constant(c) => *c,
730 EnchantedChance::Linear {
731 base,
732 per_level_above_first,
733 } => base + per_level_above_first * (level - 1) as f32,
734 }
735 } else {
736 *unenchanted_chance
737 };
738 ctx.rng.random::<f32>() < effective_chance
739 }
740 LootCondition::MatchTool(predicate) => {
741 if let Some(tool) = ctx.tool {
742 predicate.test(tool, ctx)
743 } else {
744 matches!(predicate, ToolPredicate::Any)
746 }
747 }
748 LootCondition::TableBonus {
749 enchantment,
750 chances,
751 } => {
752 let level = ctx.get_enchantment_level_by_id(enchantment);
753 let index = (level as usize).min(chances.len().saturating_sub(1));
754 let chance = chances.get(index).copied().unwrap_or(0.0);
755 ctx.rng.random::<f32>() < chance
756 }
757 LootCondition::Inverted(inner) => !inner.test(ctx),
758 LootCondition::AnyOf(conditions) => conditions.iter().any(|c| c.test(ctx)),
759 LootCondition::AllOf(conditions) => conditions.iter().all(|c| c.test(ctx)),
760 LootCondition::KilledByPlayer => ctx.killed_by_player,
761 LootCondition::EntityProperties { .. } => {
762 true
764 }
765 LootCondition::DamageSourceProperties { .. } => {
766 true
768 }
769 LootCondition::LocationCheck { .. } => {
770 true
772 }
773 LootCondition::WeatherCheck {
774 raining,
775 thundering,
776 } => {
777 let weather = ctx.weather.unwrap_or_default();
778 raining.is_none_or(|r| r == weather.raining)
779 && thundering.is_none_or(|t| t == weather.thundering)
780 }
781 LootCondition::TimeCheck { value, period } => {
782 let game_time = ctx.game_time.unwrap_or(0);
783 let time = if let Some(p) = period {
784 game_time % p
785 } else {
786 game_time
787 };
788 value.test(time as f32, ctx.rng)
789 }
790 LootCondition::ValueCheck { value, range } => {
791 let v = value.get_simple(ctx.rng);
792 range.test(v, ctx.rng)
793 }
794 LootCondition::EnchantmentActiveCheck {
795 enchantment,
796 active,
797 } => {
798 let level = ctx.get_enchantment_level_by_id(enchantment);
799 let is_active = level > 0;
800 is_active == *active
801 }
802 LootCondition::EntityScores { .. } => {
803 true
805 }
806 LootCondition::Reference(_name) => {
807 true
810 }
811 }
812 }
813}
814
815impl ToolPredicate {
816 pub fn test<R: rand::Rng>(&self, tool: &ItemStack, _ctx: &LootContext<'_, R>) -> bool {
818 match self {
819 ToolPredicate::Item(item_id) => tool.item.key == *item_id,
820 ToolPredicate::HasEnchantment {
821 enchantment,
822 min_level,
823 } => {
824 tool.get_enchantment_level(enchantment) >= *min_level
826 }
827 ToolPredicate::Tag(tag) => {
828 REGISTRY.items.is_in_tag(tool.item, tag)
830 }
831 ToolPredicate::Any => true,
832 }
833 }
834}
835
836#[derive(Debug, Clone)]
838pub enum EnchantmentOptions {
839 Tag(Identifier),
841 List(&'static [Identifier]),
843}
844
845#[derive(Debug, Clone)]
847pub struct ConditionalLootFunction {
848 pub function: LootFunction,
849 pub conditions: &'static [LootCondition],
850}
851
852#[derive(Debug, Clone)]
854pub enum LootFunction {
855 SetCount { count: NumberProvider, add: bool },
857 ExplosionDecay,
859 ApplyBonus {
861 enchantment: Identifier,
862 formula: BonusFormula,
863 },
864 EnchantedCountIncrease {
866 enchantment: Identifier,
867 count: NumberProvider,
868 limit: i32,
869 },
870 LimitCount { min: Option<i32>, max: Option<i32> },
872 SetDamage { damage: NumberProvider, add: bool },
874 EnchantRandomly { options: EnchantmentOptions },
876 EnchantWithLevels {
878 levels: NumberProvider,
879 options: EnchantmentOptions,
880 },
881 CopyComponents {
883 source: CopySource,
884 include: &'static [Identifier],
885 },
886 CopyState {
888 block: Identifier,
889 properties: &'static [&'static str],
890 },
891 SetComponents { components: &'static str },
893 SetCustomData { tag: &'static str },
895 FurnaceSmelt,
897 ExplorationMap {
899 destination: Identifier,
900 decoration: Identifier,
901 zoom: i32,
902 skip_existing_chunks: bool,
903 },
904 SetName {
906 name: &'static str,
907 target: NameTarget,
908 },
909 SetOminousBottleAmplifier { amplifier: NumberProvider },
911 SetPotion { id: Identifier },
913 SetStewEffect { effects: &'static [StewEffect] },
915 SetInstrument { options: Identifier },
917 SetEnchantments {
919 enchantments: &'static [(Identifier, NumberProvider)],
920 add: bool,
921 },
922 SetItem { item: Identifier },
924 CopyName { source: CopySource },
926 SetLore {
928 lore: &'static [&'static str],
929 mode: ListOperation,
930 },
931 SetContents {
933 entries: &'static [LootEntry],
934 component_type: Identifier,
935 },
936 ModifyContents {
938 modifier: &'static [ConditionalLootFunction],
939 component_type: Identifier,
940 },
941 SetLootTable {
943 loot_table: Identifier,
944 seed: Option<i64>,
945 },
946 SetAttributes {
948 modifiers: &'static [AttributeModifier],
949 replace: bool,
950 },
951 FillPlayerHead { entity: LootContextEntity },
953 CopyCustomData {
955 source: CopySource,
956 operations: &'static [CopyDataOperation],
957 },
958 SetBannerPattern {
960 patterns: &'static [BannerPattern],
961 append: bool,
962 },
963 SetFireworks {
965 explosions: Option<&'static [FireworkExplosion]>,
966 flight_duration: Option<i32>,
967 },
968 SetFireworkExplosion { explosion: FireworkExplosion },
970 SetBookCover {
972 title: Option<&'static str>,
973 author: Option<&'static str>,
974 generation: Option<i32>,
975 },
976 SetWrittenBookPages {
978 pages: &'static [&'static str],
979 mode: ListOperation,
980 },
981 SetWritableBookPages {
983 pages: &'static [&'static str],
984 mode: ListOperation,
985 },
986 ToggleTooltips {
988 toggles: &'static [(Identifier, bool)],
989 },
990 SetCustomModelData { value: NumberProvider },
992 Discard,
994 Reference(Identifier),
996 Sequence {
998 functions: &'static [ConditionalLootFunction],
999 },
1000 Filtered {
1002 item_filter: ToolPredicate,
1003 modifier: &'static ConditionalLootFunction,
1004 },
1005}
1006
1007#[derive(Debug, Clone, Copy)]
1009pub enum ListOperation {
1010 ReplaceAll,
1012 ReplaceSection { offset: i32, size: Option<i32> },
1014 InsertBefore { offset: i32 },
1016 InsertAfter { offset: i32 },
1018 Append,
1020}
1021
1022#[derive(Debug, Clone)]
1024pub struct AttributeModifier {
1025 pub attribute: Identifier,
1026 pub operation: AttributeOperation,
1027 pub amount: NumberProvider,
1028 pub id: Identifier,
1029 pub slot: EquipmentSlotGroup,
1030}
1031
1032#[expect(clippy::enum_variant_names, reason = "matches Vanilla naming")]
1034#[derive(Debug, Clone, Copy)]
1035pub enum AttributeOperation {
1036 AddValue,
1037 AddMultipliedBase,
1038 AddMultipliedTotal,
1039}
1040
1041#[derive(Debug, Clone)]
1043pub struct CopyDataOperation {
1044 pub source_path: &'static str,
1045 pub target_path: &'static str,
1046 pub op: CopyDataOp,
1047}
1048
1049#[derive(Debug, Clone, Copy)]
1051pub enum CopyDataOp {
1052 Replace,
1053 Append,
1054 Merge,
1055}
1056
1057#[derive(Debug, Clone)]
1059pub struct BannerPattern {
1060 pub pattern: Identifier,
1061 pub color: DyeColor,
1062}
1063
1064#[derive(Debug, Clone)]
1066pub struct FireworkExplosion {
1067 pub shape: FireworkShape,
1068 pub colors: &'static [i32],
1069 pub fade_colors: &'static [i32],
1070 pub has_trail: bool,
1071 pub has_twinkle: bool,
1072}
1073
1074#[derive(Debug, Clone, Copy)]
1076pub enum FireworkShape {
1077 SmallBall,
1078 LargeBall,
1079 Star,
1080 Creeper,
1081 Burst,
1082}
1083
1084#[derive(Debug, Clone, Copy)]
1086pub enum BonusFormula {
1087 OreDrops,
1089 UniformBonusCount { bonus_multiplier: i32 },
1091 BinomialWithBonusCount { extra: i32, probability: f32 },
1093}
1094
1095#[derive(Debug, Clone, Copy)]
1097pub enum CopySource {
1098 BlockEntity,
1099 This,
1100 Attacker,
1101 DirectAttacker,
1102}
1103
1104#[derive(Debug, Clone, Copy)]
1106pub enum NameTarget {
1107 CustomName,
1108 ItemName,
1109}
1110
1111#[derive(Debug, Clone)]
1113pub struct StewEffect {
1114 pub effect_type: Identifier,
1115 pub duration: NumberProvider,
1116}
1117
1118#[derive(Debug, Clone)]
1120pub enum LootEntry {
1121 Item {
1123 name: Identifier,
1124 weight: i32,
1125 quality: i32,
1126 conditions: &'static [LootCondition],
1127 functions: &'static [ConditionalLootFunction],
1128 },
1129 LootTableRef {
1131 name: Identifier,
1132 weight: i32,
1133 quality: i32,
1134 conditions: &'static [LootCondition],
1135 functions: &'static [ConditionalLootFunction],
1136 },
1137 InlineLootTable {
1139 pools: &'static [LootPool],
1140 weight: i32,
1141 quality: i32,
1142 conditions: &'static [LootCondition],
1143 functions: &'static [ConditionalLootFunction],
1144 },
1145 Tag {
1147 name: Identifier,
1148 expand: bool,
1149 weight: i32,
1150 quality: i32,
1151 conditions: &'static [LootCondition],
1152 functions: &'static [ConditionalLootFunction],
1153 },
1154 Alternatives {
1156 children: &'static [LootEntry],
1157 conditions: &'static [LootCondition],
1158 },
1159 Group {
1161 children: &'static [LootEntry],
1162 conditions: &'static [LootCondition],
1163 },
1164 Sequence {
1166 children: &'static [LootEntry],
1167 conditions: &'static [LootCondition],
1168 },
1169 Empty {
1171 weight: i32,
1172 conditions: &'static [LootCondition],
1173 },
1174 Dynamic {
1176 name: Identifier,
1177 conditions: &'static [LootCondition],
1178 },
1179 Slots {
1181 slots: SlotRange,
1183 conditions: &'static [LootCondition],
1184 functions: &'static [ConditionalLootFunction],
1185 },
1186}
1187
1188#[derive(Debug, Clone, Copy)]
1190pub enum SlotRange {
1191 Single(i32),
1193 Range { min: i32, max: i32 },
1195 Contents,
1197 Named(&'static [&'static str]),
1199}
1200
1201impl LootEntry {
1202 pub fn weight(&self) -> i32 {
1204 match self {
1205 Self::Item { weight, .. } => *weight,
1206 Self::LootTableRef { weight, .. } => *weight,
1207 Self::InlineLootTable { weight, .. } => *weight,
1208 Self::Tag { weight, .. } => *weight,
1209 Self::Empty { weight, .. } => *weight,
1210 Self::Alternatives { .. }
1212 | Self::Group { .. }
1213 | Self::Sequence { .. }
1214 | Self::Dynamic { .. }
1215 | Self::Slots { .. } => 1,
1216 }
1217 }
1218
1219 pub fn quality(&self) -> i32 {
1221 match self {
1222 Self::Item { quality, .. } => *quality,
1223 Self::LootTableRef { quality, .. } => *quality,
1224 Self::InlineLootTable { quality, .. } => *quality,
1225 Self::Tag { quality, .. } => *quality,
1226 Self::Empty { .. }
1227 | Self::Alternatives { .. }
1228 | Self::Group { .. }
1229 | Self::Sequence { .. }
1230 | Self::Dynamic { .. }
1231 | Self::Slots { .. } => 0,
1232 }
1233 }
1234
1235 pub fn effective_weight(&self, luck: f32) -> i32 {
1238 let base = self.weight() as f32;
1239 let quality = self.quality() as f32;
1240 (base + quality * luck).floor().max(0.0) as i32
1241 }
1242
1243 pub fn conditions(&self) -> &'static [LootCondition] {
1245 match self {
1246 Self::Item { conditions, .. } => conditions,
1247 Self::LootTableRef { conditions, .. } => conditions,
1248 Self::InlineLootTable { conditions, .. } => conditions,
1249 Self::Tag { conditions, .. } => conditions,
1250 Self::Alternatives { conditions, .. } => conditions,
1251 Self::Group { conditions, .. } => conditions,
1252 Self::Sequence { conditions, .. } => conditions,
1253 Self::Empty { conditions, .. } => conditions,
1254 Self::Dynamic { conditions, .. } => conditions,
1255 Self::Slots { conditions, .. } => conditions,
1256 }
1257 }
1258
1259 pub fn functions(&self) -> &'static [ConditionalLootFunction] {
1261 match self {
1262 Self::Item { functions, .. } => functions,
1263 Self::LootTableRef { functions, .. } => functions,
1264 Self::InlineLootTable { functions, .. } => functions,
1265 Self::Tag { functions, .. } => functions,
1266 Self::Slots { functions, .. } => functions,
1267 Self::Empty { .. }
1268 | Self::Alternatives { .. }
1269 | Self::Group { .. }
1270 | Self::Sequence { .. }
1271 | Self::Dynamic { .. } => &[],
1272 }
1273 }
1274}
1275
1276#[derive(Debug, Clone)]
1278pub struct LootPool {
1279 pub rolls: NumberProvider,
1280 pub bonus_rolls: f32,
1281 pub entries: &'static [LootEntry],
1282 pub conditions: &'static [LootCondition],
1283 pub functions: &'static [ConditionalLootFunction],
1284}
1285
1286#[derive(Debug)]
1288pub struct LootTable {
1289 pub key: Identifier,
1290 pub loot_type: LootType,
1291 pub pools: &'static [LootPool],
1292 pub functions: &'static [ConditionalLootFunction],
1293 pub random_sequence: Option<Identifier>,
1294}
1295
1296impl LootTable {
1297 pub fn get_random_items<R: rand::Rng>(&self, ctx: &mut LootContext<'_, R>) -> Vec<ItemStack> {
1310 let mut result = Vec::new();
1311 for pool in self.pools {
1312 pool.add_random_items(ctx, &mut result);
1313 }
1314
1315 if !self.functions.is_empty() {
1317 for item in &mut result {
1318 for cond_func in self.functions {
1319 if cond_func.conditions.iter().all(|c| c.test(ctx)) {
1320 cond_func.function.apply(item, ctx);
1321 }
1322 }
1323 }
1324 result.retain(|item| item.count > 0);
1326 }
1327
1328 result
1329 }
1330}
1331
1332impl LootPool {
1333 fn add_random_items<R: rand::Rng>(
1335 &self,
1336 ctx: &mut LootContext<'_, R>,
1337 result: &mut Vec<ItemStack>,
1338 ) {
1339 for condition in self.conditions {
1341 if !condition.test(ctx) {
1342 return;
1343 }
1344 }
1345
1346 let start_index = result.len();
1348
1349 let roll_count = self.rolls.get_int(ctx.rng) + (self.bonus_rolls * ctx.luck).floor() as i32;
1351
1352 for _ in 0..roll_count {
1353 self.add_random_item(ctx, result);
1354 }
1355
1356 if !self.functions.is_empty() {
1358 for item in result.iter_mut().skip(start_index) {
1359 for cond_func in self.functions {
1360 if cond_func.conditions.iter().all(|c| c.test(ctx)) {
1361 cond_func.function.apply(item, ctx);
1362 }
1363 }
1364 }
1365 result.retain(|item| item.count > 0);
1367 }
1368 }
1369
1370 fn add_random_item<R: rand::Rng>(
1372 &self,
1373 ctx: &mut LootContext<'_, R>,
1374 result: &mut Vec<ItemStack>,
1375 ) {
1376 let mut valid_entries: Vec<(&LootEntry, i32)> = Vec::new();
1378 let mut total_weight = 0;
1379
1380 for entry in self.entries {
1381 let passes_conditions = entry.conditions().iter().all(|c| c.test(ctx));
1383
1384 if !passes_conditions {
1385 continue;
1386 }
1387
1388 let weight = entry.effective_weight(ctx.luck);
1389 if weight > 0 {
1390 valid_entries.push((entry, weight));
1391 total_weight += weight;
1392 }
1393 }
1394
1395 if total_weight == 0 || valid_entries.is_empty() {
1396 return;
1397 }
1398
1399 let selected = if valid_entries.len() == 1 {
1401 valid_entries[0].0
1402 } else {
1403 let mut index = ctx.rng.random_range(0..total_weight);
1404 let mut selected_entry = valid_entries[0].0;
1405 for (entry, weight) in &valid_entries {
1406 index -= weight;
1407 if index < 0 {
1408 selected_entry = entry;
1409 break;
1410 }
1411 }
1412 selected_entry
1413 };
1414
1415 selected.create_items(ctx, result);
1417 }
1418}
1419
1420impl LootEntry {
1421 fn create_items<R: rand::Rng>(
1423 &self,
1424 ctx: &mut LootContext<'_, R>,
1425 result: &mut Vec<ItemStack>,
1426 ) {
1427 match self {
1428 LootEntry::Item {
1429 name, functions, ..
1430 } => {
1431 if let Some(item_ref) = REGISTRY.items.by_key(name) {
1432 let mut item = ItemStack::new(item_ref);
1433
1434 for cond_func in *functions {
1436 if cond_func.conditions.iter().all(|c| c.test(ctx)) {
1437 cond_func.function.apply(&mut item, ctx);
1438 }
1439 }
1440
1441 if item.count > 0 {
1442 result.push(item);
1443 }
1444 }
1445 }
1446 LootEntry::LootTableRef {
1447 name, functions, ..
1448 } => {
1449 if let Some(table) = REGISTRY.loot_tables.by_key(name) {
1451 let mut items = table.get_random_items(ctx);
1452 for item in &mut items {
1454 for cond_func in *functions {
1455 if cond_func.conditions.iter().all(|c| c.test(ctx)) {
1456 cond_func.function.apply(item, ctx);
1457 }
1458 }
1459 }
1460 result.extend(items.into_iter().filter(|i| i.count > 0));
1461 }
1462 }
1463 LootEntry::InlineLootTable {
1464 pools, functions, ..
1465 } => {
1466 let mut items = Vec::new();
1468 for pool in *pools {
1469 pool.add_random_items(ctx, &mut items);
1470 }
1471 for item in &mut items {
1473 for cond_func in *functions {
1474 if cond_func.conditions.iter().all(|c| c.test(ctx)) {
1475 cond_func.function.apply(item, ctx);
1476 }
1477 }
1478 }
1479 result.extend(items.into_iter().filter(|i| i.count > 0));
1480 }
1481 LootEntry::Tag {
1482 name,
1483 expand,
1484 functions,
1485 ..
1486 } => {
1487 if let Some(items) = REGISTRY.items.get_tag(name) {
1489 if *expand {
1490 if !items.is_empty() {
1492 let index = ctx.rng.random_range(0..items.len());
1493 let mut item = ItemStack::new(items[index]);
1494 for cond_func in *functions {
1495 if cond_func.conditions.iter().all(|c| c.test(ctx)) {
1496 cond_func.function.apply(&mut item, ctx);
1497 }
1498 }
1499 if item.count > 0 {
1500 result.push(item);
1501 }
1502 }
1503 } else {
1504 for item_ref in items {
1506 let mut item = ItemStack::new(item_ref);
1507 for cond_func in *functions {
1508 if cond_func.conditions.iter().all(|c| c.test(ctx)) {
1509 cond_func.function.apply(&mut item, ctx);
1510 }
1511 }
1512 if item.count > 0 {
1513 result.push(item);
1514 }
1515 }
1516 }
1517 }
1518 }
1519 LootEntry::Alternatives { children, .. } => {
1520 for child in *children {
1522 let passes_conditions = child.conditions().iter().all(|c| c.test(ctx));
1524 if !passes_conditions {
1525 continue; }
1527
1528 let before_len = result.len();
1529 child.create_items(ctx, result);
1530 if result.len() > before_len {
1531 break; }
1533 }
1534 }
1535 LootEntry::Group { children, .. } => {
1536 for child in *children {
1538 let passes_conditions = child.conditions().iter().all(|c| c.test(ctx));
1539 if passes_conditions {
1540 child.create_items(ctx, result);
1541 }
1542 }
1543 }
1544 LootEntry::Sequence { children, .. } => {
1545 for child in *children {
1549 let passes_conditions = child.conditions().iter().all(|c| c.test(ctx));
1550 if !passes_conditions {
1551 break; }
1553 child.create_items(ctx, result);
1554 }
1555 }
1556 LootEntry::Empty { .. } => {
1557 }
1559 LootEntry::Dynamic { name, .. } => {
1560 let _ = name;
1571 }
1572 LootEntry::Slots {
1573 slots, functions, ..
1574 } => {
1575 let _ = slots;
1582 let _ = functions;
1583 }
1584 }
1585 }
1586}
1587
1588impl LootFunction {
1589 pub fn apply<R: rand::Rng>(&self, item: &mut ItemStack, ctx: &mut LootContext<'_, R>) {
1599 match self {
1600 LootFunction::SetCount {
1601 count: provider,
1602 add,
1603 } => {
1604 let value = provider.get_int(ctx.rng);
1605 if *add {
1606 item.count += value;
1607 } else {
1608 item.count = value;
1609 }
1610 }
1611 LootFunction::ExplosionDecay => {
1612 if let Some(radius) = ctx.explosion_radius {
1613 let probability = 1.0 / radius;
1615 let mut result_count = 0;
1616 for _ in 0..item.count {
1617 if ctx.rng.random::<f32>() <= probability {
1618 result_count += 1;
1619 }
1620 }
1621 item.count = result_count;
1622 }
1623 }
1624 LootFunction::ApplyBonus {
1625 enchantment,
1626 formula,
1627 } => {
1628 let level = ctx.get_enchantment_level_by_id(enchantment);
1629 item.count = formula.apply(item.count, level, ctx.rng);
1630 }
1631 LootFunction::EnchantedCountIncrease {
1632 enchantment,
1633 count: provider,
1634 limit,
1635 } => {
1636 let level = ctx.get_enchantment_level_by_id(enchantment);
1637 if level > 0 {
1638 let bonus = (provider.get_simple(ctx.rng) * level as f32).round() as i32;
1639 let bonus = if *limit > 0 { bonus.min(*limit) } else { bonus };
1640 item.count += bonus;
1641 }
1642 }
1643 LootFunction::LimitCount { min, max } => {
1644 if let Some(min_val) = min {
1645 item.count = item.count.max(*min_val);
1646 }
1647 if let Some(max_val) = max {
1648 item.count = item.count.min(*max_val);
1649 }
1650 }
1651 LootFunction::SetDamage { damage, add } => {
1652 item.set_damage_fraction(damage.get_simple(ctx.rng), *add);
1653 }
1654 LootFunction::EnchantRandomly { options } => {
1655 item.enchant_randomly(options, ctx.rng);
1657 }
1658 LootFunction::EnchantWithLevels { levels, options } => {
1659 let level = levels.get_int(ctx.rng);
1661 item.enchant_with_levels(level, options, ctx.rng);
1662 }
1663 LootFunction::CopyComponents { source, include } => {
1664 item.copy_components(*source, include, ctx);
1666 }
1667 LootFunction::CopyState { block, properties } => {
1668 item.copy_block_state(block, properties, ctx);
1670 }
1671 LootFunction::SetComponents { components } => {
1672 item.set_components_from_json(components);
1674 }
1675 LootFunction::SetCustomData { tag } => {
1676 item.set_custom_data(tag);
1677 }
1678 LootFunction::FurnaceSmelt => {
1679 item.apply_furnace_smelt();
1681 }
1682 LootFunction::ExplorationMap {
1683 destination,
1684 decoration,
1685 zoom,
1686 skip_existing_chunks,
1687 } => {
1688 item.create_exploration_map(destination, decoration, *zoom, *skip_existing_chunks);
1690 }
1691 LootFunction::SetName { name, target } => {
1692 item.set_name(name, *target);
1694 }
1695 LootFunction::SetOminousBottleAmplifier { amplifier } => {
1696 let amp = amplifier.get_int(ctx.rng);
1697 item.set_ominous_bottle_amplifier(amp);
1698 }
1699 LootFunction::SetPotion { id } => {
1700 item.set_potion(id);
1701 }
1702 LootFunction::SetStewEffect { effects } => {
1703 item.set_stew_effects(effects, ctx.rng);
1704 }
1705 LootFunction::SetInstrument { options } => {
1706 item.set_instrument(options, ctx.rng);
1707 }
1708 LootFunction::SetEnchantments { enchantments, add } => {
1709 let resolved: Vec<_> = enchantments
1710 .iter()
1711 .map(|(key, provider)| (key.clone(), provider.get_int(ctx.rng).max(0) as u32))
1712 .collect();
1713 item.set_enchantments(&resolved, *add);
1714 }
1715 LootFunction::SetItem { item: new_item } => {
1716 item.set_item(new_item);
1717 }
1718 LootFunction::CopyName { source } => {
1719 item.copy_name(*source, ctx);
1720 }
1721 LootFunction::SetLore { lore, mode } => {
1722 item.set_lore(lore, *mode);
1723 }
1724 LootFunction::SetContents {
1725 entries,
1726 component_type,
1727 } => {
1728 item.set_contents(entries, component_type, ctx);
1729 }
1730 LootFunction::ModifyContents {
1731 modifier,
1732 component_type,
1733 } => {
1734 item.modify_contents(modifier, component_type, ctx);
1735 }
1736 LootFunction::SetLootTable { loot_table, seed } => {
1737 item.set_loot_table(loot_table, *seed);
1738 }
1739 LootFunction::SetAttributes { modifiers, replace } => {
1740 item.set_attributes(modifiers, *replace, ctx.rng);
1741 }
1742 LootFunction::FillPlayerHead { entity } => {
1743 item.fill_player_head(*entity, ctx);
1744 }
1745 LootFunction::CopyCustomData { source, operations } => {
1746 item.copy_custom_data(*source, operations, ctx);
1747 }
1748 LootFunction::SetBannerPattern { patterns, append } => {
1749 item.set_banner_pattern(patterns, *append);
1750 }
1751 LootFunction::SetFireworks {
1752 explosions,
1753 flight_duration,
1754 } => {
1755 item.set_fireworks(*explosions, *flight_duration);
1756 }
1757 LootFunction::SetFireworkExplosion { explosion } => {
1758 item.set_firework_explosion(explosion);
1759 }
1760 LootFunction::SetBookCover {
1761 title,
1762 author,
1763 generation,
1764 } => {
1765 item.set_book_cover(*title, *author, *generation);
1766 }
1767 LootFunction::SetWrittenBookPages { pages, mode } => {
1768 item.set_written_book_pages(pages, *mode);
1769 }
1770 LootFunction::SetWritableBookPages { pages, mode } => {
1771 item.set_writable_book_pages(pages, *mode);
1772 }
1773 LootFunction::ToggleTooltips { toggles } => {
1774 item.toggle_tooltips(toggles);
1775 }
1776 LootFunction::SetCustomModelData { value } => {
1777 item.set_custom_model_data(value.get_int(ctx.rng));
1778 }
1779 LootFunction::Discard => {
1780 item.count = 0;
1781 }
1782 LootFunction::Reference(_name) => {
1783 }
1785 LootFunction::Sequence { functions } => {
1786 for cond_func in *functions {
1787 if cond_func.conditions.iter().all(|c| c.test(ctx)) {
1788 cond_func.function.apply(item, ctx);
1789 }
1790 }
1791 }
1792 LootFunction::Filtered {
1793 item_filter,
1794 modifier,
1795 } => {
1796 if item_filter.test(item, ctx) && modifier.conditions.iter().all(|c| c.test(ctx)) {
1797 modifier.function.apply(item, ctx);
1798 }
1799 }
1800 }
1801 }
1802}
1803
1804impl BonusFormula {
1805 pub fn apply<R: rand::Rng>(&self, count: i32, level: i32, rng: &mut R) -> i32 {
1807 match self {
1808 BonusFormula::OreDrops => {
1809 if level > 0 {
1810 let bonus = rng.random_range(0..level + 2) - 1;
1812 let multiplier = bonus.max(0) + 1;
1813 count * multiplier
1814 } else {
1815 count
1816 }
1817 }
1818 BonusFormula::UniformBonusCount { bonus_multiplier } => {
1819 if level > 0 {
1821 count + rng.random_range(0..bonus_multiplier * level + 1)
1822 } else {
1823 count
1824 }
1825 }
1826 BonusFormula::BinomialWithBonusCount { extra, probability } => {
1827 let trials = level + extra;
1829 let mut bonus = 0;
1830 for _ in 0..trials {
1831 if rng.random::<f32>() < *probability {
1832 bonus += 1;
1833 }
1834 }
1835 count + bonus
1836 }
1837 }
1838 }
1839}
1840
1841pub type LootTableRef = &'static LootTable;
1842
1843pub struct LootTableRegistry {
1845 tables_by_id: Vec<LootTableRef>,
1846 tables_by_key: FxHashMap<Identifier, usize>,
1847 allows_registering: bool,
1848}
1849
1850impl LootTableRegistry {
1851 #[must_use]
1852 pub fn new() -> Self {
1853 Self {
1854 tables_by_id: Vec::new(),
1855 tables_by_key: FxHashMap::default(),
1856 allows_registering: true,
1857 }
1858 }
1859
1860 pub fn register(&mut self, table: LootTableRef) -> usize {
1861 assert!(
1862 self.allows_registering,
1863 "Cannot register loot tables after the registry has been frozen"
1864 );
1865
1866 let id = self.tables_by_id.len();
1867 self.tables_by_key.insert(table.key.clone(), id);
1868 self.tables_by_id.push(table);
1869 id
1870 }
1871
1872 pub fn iter(&self) -> impl Iterator<Item = (usize, LootTableRef)> + '_ {
1873 self.tables_by_id
1874 .iter()
1875 .enumerate()
1876 .map(|(id, &table)| (id, table))
1877 }
1878}
1879
1880impl Default for LootTableRegistry {
1881 fn default() -> Self {
1882 Self::new()
1883 }
1884}
1885
1886crate::impl_registry!(
1887 LootTableRegistry,
1888 LootTable,
1889 tables_by_id,
1890 tables_by_key,
1891 loot_tables
1892);
1893
1894#[cfg(test)]
1895mod tests {
1896 use crate::{Registry, vanilla_blocks, vanilla_items, vanilla_loot_tables};
1897
1898 use super::*;
1899 use rand::SeedableRng;
1900
1901 fn test_rng() -> rand::rngs::StdRng {
1902 rand::rngs::StdRng::seed_from_u64(12345)
1903 }
1904
1905 fn init_test_registries() {
1906 REGISTRY.get_or_init(|| {
1907 let mut registry = Registry::new_empty();
1908 vanilla_loot_tables::register_loot_tables(&mut registry.loot_tables);
1909 vanilla_items::register_items(&mut registry.items);
1910 vanilla_blocks::register_blocks(&mut registry.blocks);
1911 registry.freeze();
1912 registry
1913 });
1914 }
1915
1916 #[test]
1917 fn test_oak_log_loot() {
1918 init_test_registries();
1919 let mut rng = test_rng();
1920
1921 let mut ctx = LootContext::new(&mut rng);
1922 let items = vanilla_loot_tables::BLOCKS_OAK_LOG.get_random_items(&mut ctx);
1923
1924 assert_eq!(items.len(), 1);
1925 assert_eq!(items[0].count, 1);
1926 assert_eq!(items[0].item.key, Identifier::vanilla_static("oak_log"));
1927 }
1928
1929 #[test]
1930 fn test_diamond_ore_loot_no_silk_touch() {
1931 init_test_registries();
1933 let mut rng = test_rng();
1934
1935 let mut ctx = LootContext::new(&mut rng);
1936 let items = vanilla_loot_tables::BLOCKS_DIAMOND_ORE.get_random_items(&mut ctx);
1937
1938 assert_eq!(items.len(), 1);
1939 assert_eq!(items[0].count, 1);
1940 assert_eq!(items[0].item.key, Identifier::vanilla_static("diamond"));
1942 }
1943
1944 #[test]
1945 fn test_grass_block_loot_no_silk_touch() {
1946 init_test_registries();
1948 let mut rng = test_rng();
1949
1950 let mut ctx = LootContext::new(&mut rng);
1951 let items = vanilla_loot_tables::BLOCKS_GRASS_BLOCK.get_random_items(&mut ctx);
1952
1953 assert_eq!(items.len(), 1);
1954 assert_eq!(items[0].count, 1);
1955 assert_eq!(items[0].item.key, Identifier::vanilla_static("dirt"));
1957 }
1958
1959 #[test]
1960 fn test_stone_loot_no_silk_touch() {
1961 init_test_registries();
1963 let mut rng = test_rng();
1964
1965 let mut ctx = LootContext::new(&mut rng);
1966 let items = vanilla_loot_tables::BLOCKS_STONE.get_random_items(&mut ctx);
1967
1968 assert_eq!(items.len(), 1);
1969 assert_eq!(items[0].count, 1);
1970 assert_eq!(items[0].item.key, Identifier::vanilla_static("cobblestone"));
1972 }
1973
1974 #[test]
1975 fn test_explosion_decay_function() {
1976 init_test_registries();
1978
1979 let cond_func = ConditionalLootFunction {
1981 function: LootFunction::ExplosionDecay,
1982 conditions: &[],
1983 };
1984
1985 let mut total_survived = 0;
1986 let initial_count = 10;
1987
1988 for seed in 0u64..100 {
1989 let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
1990 let mut ctx = LootContext::new(&mut rng).with_explosion(4.0);
1991 let mut item = ItemStack::with_count(&crate::vanilla_items::ITEMS.stone, initial_count);
1992 cond_func.function.apply(&mut item, &mut ctx);
1993 total_survived += item.count;
1994 }
1995
1996 assert!(
2000 total_survived > 150 && total_survived < 350,
2001 "Expected ~250 items with explosion decay (25% of 1000), got {}",
2002 total_survived
2003 );
2004 }
2005
2006 #[test]
2007 fn test_survives_explosion_condition() {
2008 init_test_registries();
2009
2010 let mut survived = 0;
2013 for seed in 0..100 {
2014 let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
2015 let mut ctx = LootContext::new(&mut rng).with_explosion(4.0);
2016 let items = vanilla_loot_tables::BLOCKS_GRAVEL.get_random_items(&mut ctx);
2017 if !items.is_empty() {
2018 survived += 1;
2019 }
2020 }
2021
2022 assert!(
2024 survived > 10 && survived < 50,
2025 "Expected ~25% survival rate, got {}%",
2026 survived
2027 );
2028 }
2029}