Skip to main content

yog_registry/
loot.rs

1//! Loot: what a loot table gave — a chest's, a block's drops, a mob's —
2//! as the mods see it before it's handed out ([`Loot`]), and what a mod
3//! changes in it declaratively ([`LootModifier`], a `.ydef`).
4
5use yog_data::kinds::Item;
6use yog_data::with::{List, RefTo};
7
8// A loot table: `@loot_table:minecraft:chests/simple_dungeon`.
9yog_data::kind! {
10    LootTable = "loot_table",
11}
12
13/// A stack of items: its id, how many, and its custom data (SNBT, `{}` for none).
14#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
15pub struct LootStack {
16    pub id: String,
17    pub count: u32,
18    #[data(default)]
19    pub nbt: String,
20}
21
22impl LootStack {
23    pub fn new(id: impl Into<String>, count: u32) -> Self {
24        Self { id: id.into(), count, nbt: String::new() }
25    }
26}
27
28/// A loot table rolled: where and for whom, and what it gave. Handlers
29/// change `items`; each sees the one before's result.
30#[derive(yog_data::Schema, Debug, Clone, Default, PartialEq)]
31pub struct Loot {
32    /// The table (`minecraft:chests/simple_dungeon`, `minecraft:blocks/amethyst_cluster`).
33    pub table: String,
34    /// The dimension it's rolled in (`minecraft:overworld`).
35    #[data(default)]
36    pub dimension: String,
37    /// Where: the chest, the broken block, the mob.
38    pub pos: Option<[f64; 3]>,
39    /// The entity it's for: the mob killed, the player opening the chest.
40    pub entity: Option<String>,
41    /// Who killed it (an entity's uuid).
42    pub killer: Option<String>,
43    /// The tool breaking the block.
44    pub tool: Option<LootStack>,
45    /// The tool's enchantments and levels (`minecraft:fortune`, 3).
46    #[data(default)]
47    pub enchantments: Vec<(String, u32)>,
48    /// What it gave.
49    #[data(default)]
50    pub items: Vec<LootStack>,
51}
52
53impl Loot {
54    /// The tool's level of `enchantment` (`minecraft:fortune`), 0 without it.
55    pub fn enchantment(&self, enchantment: &str) -> u32 {
56        self.enchantments.iter().find(|(e, _)| e == enchantment).map_or(0, |(_, l)| *l)
57    }
58}
59
60/// Changes a mod makes to the loot of some tables: adds items, scales
61/// some. A `.ydef` (`kind: "loot_modifier"`) or built in code.
62#[derive(yog_data::Schema, Debug, Clone, Default, PartialEq)]
63pub struct LootModifier {
64    pub id: String,
65    /// The tables it changes (`@loot_table:minecraft:chests/simple_dungeon`).
66    #[data(with = "List<RefTo<LootTable>>")]
67    pub tables: Vec<String>,
68    /// Items added.
69    #[data(default)]
70    pub add: Vec<LootAdd>,
71    /// Items whose counts are scaled.
72    #[data(default)]
73    pub scale: Vec<LootScale>,
74}
75
76/// An item a [`LootModifier`] adds.
77#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
78pub struct LootAdd {
79    #[data(with = "RefTo<Item>")]
80    pub item: String,
81    /// How many: from `count[0]` to `count[1]`, evenly; below 1 adds none.
82    #[data(default = "one_each")]
83    pub count: [i32; 2],
84    /// The chance it's added at all (0–1).
85    #[data(default = "always")]
86    #[data(range = "0..=1")]
87    pub chance: f64,
88    /// A loot function of the mod's code that finishes the stack (its
89    /// custom data): registered by name.
90    pub function: Option<String>,
91}
92
93/// Item counts a [`LootModifier`] scales.
94#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
95pub struct LootScale {
96    #[data(with = "RefTo<Item>")]
97    pub item: String,
98    /// The factor; the count is rounded down (`0.5`: half, at least none).
99    #[data(range = "0..=64")]
100    pub by: f64,
101}
102
103fn one_each() -> [i32; 2] {
104    [1, 1]
105}
106
107fn always() -> f64 {
108    1.0
109}
110
111impl LootModifier {
112    /// Applies it to `loot` (if its table is one of `tables`): `random`
113    /// gives numbers in `[0, 1)`; `finish` runs a named loot function on a
114    /// stack it adds.
115    pub fn apply(&self, loot: &mut Loot, random: &mut dyn FnMut() -> f64, finish: &mut dyn FnMut(&str, &mut LootStack, &Loot)) {
116        if !self.tables.iter().any(|t| *t == loot.table) {
117            return;
118        }
119        for s in &self.scale {
120            for stack in loot.items.iter_mut().filter(|st| st.id == s.item) {
121                stack.count = (stack.count as f64 * s.by).floor() as u32;
122            }
123        }
124        loot.items.retain(|st| st.count > 0);
125        for a in &self.add {
126            if a.chance < 1.0 && random() >= a.chance {
127                continue;
128            }
129            let (lo, hi) = (a.count[0].min(a.count[1]), a.count[0].max(a.count[1]));
130            let n = lo + (random() * (hi - lo + 1) as f64) as i32;
131            if n < 1 {
132                continue;
133            }
134            let mut stack = LootStack::new(a.item.clone(), n as u32);
135            if let Some(f) = &a.function {
136                finish(f, &mut stack, loot);
137            }
138            loot.items.push(stack);
139        }
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn a_modifier_scales_and_adds_on_its_tables() {
149        let m = LootModifier {
150            id: "m".into(),
151            tables: vec!["minecraft:chests/simple_dungeon".into()],
152            add: vec![LootAdd { item: "mymod:scroll".into(), count: [-1, 1], chance: 1.0, function: Some("mymod:pattern".into()) }],
153            scale: vec![LootScale { item: "minecraft:amethyst_shard".into(), by: 0.5 }],
154        };
155        let mut loot = Loot { table: "minecraft:chests/simple_dungeon".into(), items: vec![LootStack::new("minecraft:amethyst_shard", 3)], ..Default::default() };
156        let mut r = [0.99].into_iter();
157        m.apply(&mut loot, &mut || r.next().unwrap(), &mut |f, s, _| s.nbt = format!("{{f:\"{f}\"}}"));
158        assert_eq!(loot.items[0].count, 1, "3 × 0.5, rounded down");
159        assert_eq!((loot.items[1].id.as_str(), loot.items[1].count, loot.items[1].nbt.as_str()), ("mymod:scroll", 1, "{f:\"mymod:pattern\"}"));
160
161        let mut none = Loot { table: "minecraft:chests/simple_dungeon".into(), ..Default::default() };
162        m.apply(&mut none, &mut || 0.0, &mut |_, _, _| {});
163        assert!(none.items.is_empty(), "-1 adds nothing");
164
165        let mut other = Loot { table: "minecraft:chests/igloo_chest".into(), items: vec![LootStack::new("minecraft:amethyst_shard", 3)], ..Default::default() };
166        m.apply(&mut other, &mut || 0.0, &mut |_, _, _| {});
167        assert_eq!(other.items[0].count, 3, "another table");
168    }
169
170    #[test]
171    fn loot_crosses_as_yog_data() {
172        use yog_data::Schema;
173        let loot = Loot {
174            table: "minecraft:blocks/amethyst_cluster".into(),
175            pos: Some([1.0, 2.0, 3.0]),
176            tool: Some(LootStack::new("minecraft:diamond_pickaxe", 1)),
177            enchantments: vec![("minecraft:fortune".into(), 3)],
178            items: vec![LootStack::new("minecraft:amethyst_shard", 4)],
179            ..Default::default()
180        };
181        let bytes = yog_data::binary::encode(&loot.write());
182        let back = yog_data::read_bytes::<Loot>(&bytes).unwrap();
183        assert_eq!(back, loot);
184        assert_eq!(back.enchantment("minecraft:fortune"), 3);
185    }
186}