enshwick 1.0.0-alpha.11

The Enshwick Loader's modding API for Enshrouded: the one crate mods are written against
Documentation
use super::*;

fn roll(kind: Kinds) -> Roll {
    Roll {
        kind,
        entity: Some(Entity(5)),
        node: None,
        labels: None,
        boss: false,
        locked: false,
    }
}

#[test]
fn filters_keep_their_kind_and_markers() {
    let corpse = roll(Kinds::CORPSES);
    let boss = Roll {
        boss: true,
        labels: Some(LootContainerId::new(3)),
        ..corpse
    };
    let chest = Roll {
        node: Some(LootContainerId::new(9)),
        ..roll(Kinds::CHESTS)
    };
    let locked = Roll {
        locked: true,
        ..chest
    };
    assert!(Source::kinds(Kinds::CONTAINERS).matches(&corpse));
    assert!(Source::kinds(Kinds::CONTAINERS).matches(&chest));
    assert!(!Source::kinds(Kinds::CONTAINERS).matches(&roll(Kinds::DESTROYED)));
    assert!(Source::bosses().matches(&boss) && !Source::bosses().matches(&corpse));
    assert!(!Source::bosses().matches(&locked));
    assert!(Source::locked_chests().matches(&locked));
    assert!(!Source::locked_chests().matches(&chest));
    assert!(
        Source::bosses()
            .with_labels(&[LootContainerId::new(3)])
            .matches(&boss)
    );
    assert!(
        !Source::bosses()
            .with_labels(&[LootContainerId::new(4)])
            .matches(&boss)
    );
    assert!(
        Source::kinds(Kinds::CHESTS)
            .with_nodes(&[LootContainerId::new(9)])
            .matches(&chest)
    );
    assert!(
        !Source::kinds(Kinds::CHESTS)
            .with_nodes(&[LootContainerId::new(8)])
            .matches(&chest)
    );
}

#[test]
fn a_roll_without_labels_or_a_node_matches_no_list_of_them() {
    let corpse = roll(Kinds::CORPSES);
    let any = LootContainerId::default();
    assert!(
        !Source::kinds(Kinds::CORPSES)
            .with_labels(&[any])
            .matches(&corpse)
    );
    assert!(
        !Source::kinds(Kinds::CORPSES)
            .with_nodes(&[any])
            .matches(&corpse)
    );
    assert!(Source::kinds(Kinds::CORPSES).matches(&corpse));
}

#[test]
fn a_single_roll_slot_holds_the_item_and_count_at_their_offsets() {
    let slot = Slot::new(0, 4, 16).unwrap();
    let (bytes, len) = slot.fill(0x0102_0304, 7);
    assert_eq!(len, 16);
    assert_eq!(bytes[..6], [4, 3, 2, 1, 7, 0]);
    assert!(bytes[6..].iter().all(|&b| b == 0));
    let moved = Slot::new(8, 2, 12).unwrap();
    let (bytes, len) = moved.fill(9, 3);
    assert_eq!((len, bytes[8], bytes[2]), (12, 9, 3));
}

#[test]
fn a_single_roll_slot_that_cannot_hold_its_fields_is_refused() {
    assert!(Slot::new(0, 4, 5).is_none(), "count past the end");
    assert!(Slot::new(14, 4, 16).is_none(), "item past the end");
    assert!(
        Slot::new(0, 4, SLOT_MAX + 1).is_none(),
        "larger than the buffer"
    );
    assert!(Slot::new(usize::MAX, 4, 16).is_none(), "overflow");
    assert!(Slot::new(0, 2, 16).is_none(), "count inside the item id");
    assert!(Slot::new(3, 2, 16).is_none(), "item id over the count");
    assert!(
        Slot::new(4, 2, 16).is_some(),
        "count just before the item id"
    );
}

#[test]
fn invalid_drops_are_refused() {
    let from = Source::bosses();
    assert!(Drop::new(ItemId::new(7), 0.5, from.clone()).check().is_ok());
    assert!(
        Drop::new(ItemId::new(0), 0.5, from.clone())
            .check()
            .is_err()
    );
    assert!(
        Drop::new(ItemId::new(7), 1.5, from.clone())
            .check()
            .is_err()
    );
    assert!(
        Drop::new(ItemId::new(7), f64::NAN, from.clone())
            .check()
            .is_err()
    );
    assert!(
        Drop::new(ItemId::new(7), 0.5, from.clone())
            .count(0, 1)
            .check()
            .is_err()
    );
    assert!(
        Drop::new(ItemId::new(7), 0.5, from.clone())
            .count(3, 2)
            .check()
            .is_err()
    );
    assert!(
        Drop::new(ItemId::new(7), 0.5, from.clone())
            .meter(1, 5, 5)
            .check()
            .is_ok()
    );
    assert!(
        Drop::new(ItemId::new(7), 0.5, from.clone())
            .meter(3, 2, 5)
            .check()
            .is_err()
    );
    assert!(
        Drop::new(ItemId::new(7), 0.5, from.clone())
            .meter(1, 6, 5)
            .check()
            .is_err()
    );
    assert!(
        Drop::new(ItemId::new(7), 0.5, from)
            .meter(0, 0, 0)
            .check()
            .is_err()
    );
}

#[test]
fn meters_roll_every_count_between_their_bounds() {
    let dice = Dice(AtomicU64::new(3));
    let drop = Drop::new(ItemId::new(7), 1.0, Source::bosses()).meter(2, 4, 5);
    let mut seen = [0usize; 6];
    for _ in 0..30_000 {
        let (_, _, fill) = drop.draw(&dice).unwrap();
        let charges = (5.0 * fill.unwrap().fraction() + 0.5).floor();
        #[expect(
            clippy::cast_possible_truncation,
            clippy::cast_sign_loss,
            reason = "0 to 5"
        )]
        let charges = charges as usize;
        seen[charges] += 1;
    }
    assert_eq!((seen[0], seen[1], seen[5]), (0, 0, 0));
    assert!(
        seen[2..5].iter().all(|&n| (9_000..11_000).contains(&n)),
        "{seen:?}"
    );
}

#[test]
fn the_chance_holds_over_many_rolls() {
    let dice = Dice(AtomicU64::new(42));
    let drop = Drop::new(ItemId::new(7), 0.1, Source::bosses()).count(2, 4);
    let mut hits = 0;
    for _ in 0..100_000 {
        if let Some((item, n, fill)) = drop.draw(&dice) {
            assert_eq!((item, fill), (7, None));
            assert!((2..=4).contains(&n));
            hits += 1;
        }
    }
    assert!((9_000..11_000).contains(&hits), "{hits}");
    let never = Drop::new(ItemId::new(7), 0.0, Source::bosses());
    let always = Drop::new(ItemId::new(7), 1.0, Source::bosses());
    for _ in 0..1000 {
        assert_eq!(never.draw(&dice), None);
        assert_eq!(always.draw(&dice), Some((7, 1, None)));
    }
}

#[test]
fn one_of_spreads_over_its_items() {
    let dice = Dice(AtomicU64::new(7));
    let drop = Drop::one_of(
        &[ItemId::new(1), ItemId::new(2), ItemId::new(3)],
        1.0,
        Source::bosses(),
    );
    let mut seen = [0usize; 3];
    for _ in 0..30_000 {
        let (item, _, _) = drop.draw(&dice).unwrap();
        seen[item as usize - 1] += 1;
    }
    assert!(
        seen.iter().all(|&n| (9_000..11_000).contains(&n)),
        "{seen:?}"
    );
    assert!(Drop::one_of(&[], 0.5, Source::bosses()).check().is_err());
    assert!(
        Drop::one_of(&[ItemId::new(1), ItemId::new(0)], 0.5, Source::bosses())
            .check()
            .is_err()
    );
}