use yog_data::kinds::Item;
use yog_data::with::{List, RefTo};
yog_data::kind! {
LootTable = "loot_table",
}
#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
pub struct LootStack {
pub id: String,
pub count: u32,
#[data(default)]
pub nbt: String,
}
impl LootStack {
pub fn new(id: impl Into<String>, count: u32) -> Self {
Self { id: id.into(), count, nbt: String::new() }
}
}
#[derive(yog_data::Schema, Debug, Clone, Default, PartialEq)]
pub struct Loot {
pub table: String,
#[data(default)]
pub dimension: String,
pub pos: Option<[f64; 3]>,
pub entity: Option<String>,
pub killer: Option<String>,
pub tool: Option<LootStack>,
#[data(default)]
pub enchantments: Vec<(String, u32)>,
#[data(default)]
pub items: Vec<LootStack>,
}
impl Loot {
pub fn enchantment(&self, enchantment: &str) -> u32 {
self.enchantments.iter().find(|(e, _)| e == enchantment).map_or(0, |(_, l)| *l)
}
}
#[derive(yog_data::Schema, Debug, Clone, Default, PartialEq)]
pub struct LootModifier {
pub id: String,
#[data(with = "List<RefTo<LootTable>>")]
pub tables: Vec<String>,
#[data(default)]
pub add: Vec<LootAdd>,
#[data(default)]
pub scale: Vec<LootScale>,
}
#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
pub struct LootAdd {
#[data(with = "RefTo<Item>")]
pub item: String,
#[data(default = "one_each")]
pub count: [i32; 2],
#[data(default = "always")]
#[data(range = "0..=1")]
pub chance: f64,
pub function: Option<String>,
}
#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
pub struct LootScale {
#[data(with = "RefTo<Item>")]
pub item: String,
#[data(range = "0..=64")]
pub by: f64,
}
fn one_each() -> [i32; 2] {
[1, 1]
}
fn always() -> f64 {
1.0
}
impl LootModifier {
pub fn apply(&self, loot: &mut Loot, random: &mut dyn FnMut() -> f64, finish: &mut dyn FnMut(&str, &mut LootStack, &Loot)) {
if !self.tables.iter().any(|t| *t == loot.table) {
return;
}
for s in &self.scale {
for stack in loot.items.iter_mut().filter(|st| st.id == s.item) {
stack.count = (stack.count as f64 * s.by).floor() as u32;
}
}
loot.items.retain(|st| st.count > 0);
for a in &self.add {
if a.chance < 1.0 && random() >= a.chance {
continue;
}
let (lo, hi) = (a.count[0].min(a.count[1]), a.count[0].max(a.count[1]));
let n = lo + (random() * (hi - lo + 1) as f64) as i32;
if n < 1 {
continue;
}
let mut stack = LootStack::new(a.item.clone(), n as u32);
if let Some(f) = &a.function {
finish(f, &mut stack, loot);
}
loot.items.push(stack);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_modifier_scales_and_adds_on_its_tables() {
let m = LootModifier {
id: "m".into(),
tables: vec!["minecraft:chests/simple_dungeon".into()],
add: vec![LootAdd { item: "mymod:scroll".into(), count: [-1, 1], chance: 1.0, function: Some("mymod:pattern".into()) }],
scale: vec![LootScale { item: "minecraft:amethyst_shard".into(), by: 0.5 }],
};
let mut loot = Loot { table: "minecraft:chests/simple_dungeon".into(), items: vec![LootStack::new("minecraft:amethyst_shard", 3)], ..Default::default() };
let mut r = [0.99].into_iter();
m.apply(&mut loot, &mut || r.next().unwrap(), &mut |f, s, _| s.nbt = format!("{{f:\"{f}\"}}"));
assert_eq!(loot.items[0].count, 1, "3 × 0.5, rounded down");
assert_eq!((loot.items[1].id.as_str(), loot.items[1].count, loot.items[1].nbt.as_str()), ("mymod:scroll", 1, "{f:\"mymod:pattern\"}"));
let mut none = Loot { table: "minecraft:chests/simple_dungeon".into(), ..Default::default() };
m.apply(&mut none, &mut || 0.0, &mut |_, _, _| {});
assert!(none.items.is_empty(), "-1 adds nothing");
let mut other = Loot { table: "minecraft:chests/igloo_chest".into(), items: vec![LootStack::new("minecraft:amethyst_shard", 3)], ..Default::default() };
m.apply(&mut other, &mut || 0.0, &mut |_, _, _| {});
assert_eq!(other.items[0].count, 3, "another table");
}
#[test]
fn loot_crosses_as_yog_data() {
use yog_data::Schema;
let loot = Loot {
table: "minecraft:blocks/amethyst_cluster".into(),
pos: Some([1.0, 2.0, 3.0]),
tool: Some(LootStack::new("minecraft:diamond_pickaxe", 1)),
enchantments: vec![("minecraft:fortune".into(), 3)],
items: vec![LootStack::new("minecraft:amethyst_shard", 4)],
..Default::default()
};
let bytes = yog_data::binary::encode(&loot.write());
let back = yog_data::read_bytes::<Loot>(&bytes).unwrap();
assert_eq!(back, loot);
assert_eq!(back.enchantment("minecraft:fortune"), 3);
}
}