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