1use yog_data::kinds::Item;
6use yog_data::with::{List, RefTo};
7
8yog_data::kind! {
10 LootTable = yog_ids::kind::LOOT_TABLE,
11}
12
13#[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#[derive(yog_data::Schema, Debug, Clone, Default, PartialEq)]
35pub struct Loot {
36 pub table: String,
38 #[data(default)]
40 pub dimension: String,
41 pub pos: Option<[f64; 3]>,
43 pub entity: Option<String>,
45 pub killer: Option<String>,
47 pub tool: Option<LootStack>,
49 #[data(default)]
51 pub enchantments: Vec<(String, u32)>,
52 #[data(default)]
54 pub items: Vec<LootStack>,
55}
56
57impl Loot {
58 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#[derive(yog_data::Schema, Debug, Clone, Default, PartialEq)]
70pub struct LootModifier {
71 pub id: String,
72 #[data(with = "List<RefTo<LootTable>>")]
74 pub tables: Vec<String>,
75 #[data(default)]
77 pub add: Vec<LootAdd>,
78 #[data(default)]
80 pub scale: Vec<LootScale>,
81}
82
83#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
85pub struct LootAdd {
86 #[data(with = "RefTo<Item>")]
87 pub item: String,
88 #[data(default = "one_each")]
90 pub count: [i32; 2],
91 #[data(default = "always")]
93 #[data(range = "0..=1")]
94 pub chance: f64,
95 pub function: Option<String>,
98}
99
100#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
102pub struct LootScale {
103 #[data(with = "RefTo<Item>")]
104 pub item: String,
105 #[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 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}