use yog_data::kinds::Item;
use yog_data::with::{List, RefTo};
yog_data::kind! {
LootTable = yog_ids::kind::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.contains(&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);
}
}