1use yog_data::kinds::Item;
6use yog_data::with::{List, RefTo};
7
8yog_data::kind! {
10 LootTable = "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 { id: id.into(), count, nbt: String::new() }
25 }
26}
27
28#[derive(yog_data::Schema, Debug, Clone, Default, PartialEq)]
31pub struct Loot {
32 pub table: String,
34 #[data(default)]
36 pub dimension: String,
37 pub pos: Option<[f64; 3]>,
39 pub entity: Option<String>,
41 pub killer: Option<String>,
43 pub tool: Option<LootStack>,
45 #[data(default)]
47 pub enchantments: Vec<(String, u32)>,
48 #[data(default)]
50 pub items: Vec<LootStack>,
51}
52
53impl Loot {
54 pub fn enchantment(&self, enchantment: &str) -> u32 {
56 self.enchantments.iter().find(|(e, _)| e == enchantment).map_or(0, |(_, l)| *l)
57 }
58}
59
60#[derive(yog_data::Schema, Debug, Clone, Default, PartialEq)]
63pub struct LootModifier {
64 pub id: String,
65 #[data(with = "List<RefTo<LootTable>>")]
67 pub tables: Vec<String>,
68 #[data(default)]
70 pub add: Vec<LootAdd>,
71 #[data(default)]
73 pub scale: Vec<LootScale>,
74}
75
76#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
78pub struct LootAdd {
79 #[data(with = "RefTo<Item>")]
80 pub item: String,
81 #[data(default = "one_each")]
83 pub count: [i32; 2],
84 #[data(default = "always")]
86 #[data(range = "0..=1")]
87 pub chance: f64,
88 pub function: Option<String>,
91}
92
93#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
95pub struct LootScale {
96 #[data(with = "RefTo<Item>")]
97 pub item: String,
98 #[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 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}