3 releases (breaking)

0.7.0 Mar 13, 2023
0.4.0 Mar 8, 2023
0.3.0 Mar 7, 2023

#1085 in Algorithms

MIT/Apache

50KB
797 lines

Lootr

Lootr \lutʁ\ is a simple RPG-like looting system.

Lootr provides a way to organize data commonly named loot in a gameplay context. It helps you to manage which items can be found, in a generic and statisticaly controlled system.

It is heavily inspired from the work of Bob Nystrom http://journal.stuffwithstuff.com/2014/07/05/dropping-loot

A JS version is available here https://github.com/vincent/lootr
A C# version is available here https://github.com/Sparox/LootrCsharp

In Lootr, lootables are organized in a tree of categories and items.

ROOT
├─ Staff
├─ equipment
│  ├─ Glove
│  │  Boots
│  └─ leather
│     │  Jacket
│     └─ Pads
└─ weapons
   ├─ Bat
   └─ Knife

Then, a collection of Drops describe how items are yield from a loot action.

equipment: .5 chances, stack of 1
equipment: .2 chances, stack of 2
equipment: .1 chances, stack of 2

This might yield items in the equipment tree, for example

  • 1 Boots, once every 2 rolls on average
  • 2 Glove, once every 5 rolls
  • 1 Knife, once every 10 rolls

Create a loot bag

Create items.

use lootr::{Lootr, item::Item};
let mut loot = Lootr::new();

loot.add(
    Item::a("Berries")
);

Items can have properties.

use lootr::{Lootr, item::{Item, Props}};
let mut loot = Lootr::new();

let item = Item::from("crown", Props::from([
    ("strength", "10"),
    ("charisma", "+100")
]));

loot.add(item);

// Items can printed
// crown{strength=10,charisma=+100}

Each level is composed by a list of .items and nested .branchs.

Organize the loot repository by adding branchs

use lootr::Lootr;
let mut loot = Lootr::new();

let weapons = loot.add_branch("weapons", Lootr::new());
let armor = loot.add_branch("armor", Lootr::new());

Optionnaly with items

use lootr::{Lootr, item::Item};
let mut loot = Lootr::new();

loot.add_branch("weapons", Lootr::from(vec![
    Item::a("Staff"),
    Item::an("Uzi")
]));

loot.add_branch("armor", Lootr::from(vec![
    Item::a("Boots"),
    Item::a("Socks")
]));

loot.branch_mut("armor")
    .unwrap()
    .add_branch("leather", Lootr::from(vec![
        Item::a("Belt"),
        Item::a("Hat")
    ]));

Looting

Loot against a loot table, described by a like the following.

use lootr::{ROOT, drops::Drop};

let drops = [
    Drop { path: ROOT, depth: 1, luck: 1.0, stack: 1..=1, modify: false },
];

A builder pattern is also available to ease drops creation.

  • path() selects the root of this drop
  • depth() max depth to consider
  • luck() the luck we start with, will decrease at each sub tree
  • stack() the range of copies to yield
use lootr::{Lootr, item::Item, drops::DropBuilder};
let mut loot = Lootr::new();

loot.add_branch("weapons", Lootr::from(vec![
    Item::a("Staff"),
    Item::an("Uzi")
]));

loot.add_branch("armor", Lootr::from(vec![
    Item::a("Boots"),
    Item::a("Socks")
]));

let drops = [
    DropBuilder::new()
        .path("armor")
        .luck(1.0)
        .build(),

    DropBuilder::new()
        .path("weapons")
        .luck(1.0)
        .stack(1..=3)
        .build(),
];

// Loot your reward from a dead monster
let rewards = loot.loot(&drops);

// rewards = [ "Berries", "Plates", "Uzi", "Uzi", "Staff" ]

Seeded RNG

Lootr.loot_seeded() takes a PRNG arguments to yield items in a consitent and reproductible way.

use lootr::{Lootr, item::Item, drops::DropBuilder};
use rand_chacha::ChaCha20Rng;
use rand::SeedableRng;

(0..10).for_each(|_f| {
    let mut loot = Lootr::from(vec![
        Item::named("Socks"),
        Item::named("Boots"),
    ]);
    let drops = [DropBuilder::new().build()];

    let rng = &mut ChaCha20Rng::seed_from_u64(123);

    loot.loot_seeded(&drops, rng);
    loot.loot_seeded(&drops, rng);
    // ...

    // Will always loot Boots, then Socks, then Socks, then Boots ..
})

Modifiers

Lootr.add_modifier() allows to give some Item transformers, call Modifiers.

Modifiers are simple functions that return a new Item from a given one.

use lootr::{Lootr, item::{Item, Props}, drops::DropBuilder};
let mut loot = Lootr::new();
loot.add(Item::a("crown"));

fn with_strength(source: Item) -> Item {
    source.extend(source.name, Props::from([
        ("strength", "10"),
    ]))
}

loot.add_modifier(with_strength);

//
// Then, at loot time:

let drops = [DropBuilder::new().modify().build()];

let rewards = loot.loot(&drops);

// rewards = [ crown{strength=10} ]

Macros

To make tha building easier, you can use the bag! macro.

use lootr::{bag, Lootr, item::{Item, Props}};
let loot = bag! {
    @Weapons
        Knife attack="1" desc="A simple knife",
        @Wooden
            BarkShield attack="0" magic_power="10" desc="A wooden shield reinforced with bark, providing magic power",
            @Staffs
                WoodenStaff attack="5" magic_power="10" desc="A wooden staff imbued with magic power",
                CrystalStaff attack="8" magic_power="15" ice_damage="10" desc="A crystal staff with ice elemental damage",
                ElementalStaff attack="12" magic_power="20" thunder_damage="15" desc="An elemental staff with thunder elemental damage",
                .
            @Bows
                ShortBow attack="10" accuracy="10" desc="A short bow with high accuracy",
                LongBow attack="20" accuracy="20" ice_damage="10" desc="A long bow with ice elemental damage",
                .
            .
        @Swords
            ShortSword attack="10" critical="5" desc="A short sword with increased critical hit rate",
            LongSword attack="15" critical="10" desc="A long sword with a high critical hit rate",
            TwoHandedSword attack="20" critical="15" desc="A two-handed sword with a very high critical hit rate",
            .
        @Axes
            BattleAxe attack="12" critical="8" desc="A battle axe with increased critical hit rate",
            WarAxe attack="14" critical="9" desc="A war axe with a high critical hit rate",
            .
        @Mace
            MorningStar attack="13" critical="7" desc="A mace with increased critical hit rate",
            Flail attack="16" critical="11" desc="A flail with a very high critical hit rate",
            .
        .
    @Armors
        Shirt defense="0" desc="A simple shirt",
        @LightArmor
            LeatherArmor defense="5" agility="2" desc="Armor made of leather with increased agility",
            Chainmail defense="8" agility="1" desc="Armor made of interlocking rings with moderate agility",
            .
        @HeavyArmor
            PlateArmor defense="10" agility="-2" desc="Heavy armor made of plates with decreased agility",
            FullPlateArmor defense="15" agility="-5" desc="Very heavy armor made of plates with greatly decreased agility",
            .
        .
    @Consumables
        Water healing="2" desc="Just water",
        @Potion
            HealthPotion healing="20" desc="A potion that restores a small amount of health",
            GreaterHealthPotion healing="40" desc="A potion that restores a moderate amount of health",
            ManaPotion mana_restoration="20" desc="A potion that restores a small amount of mana",
            GreaterManaPotion mana_restoration="40" desc="A potion that restores a moderate amount of mana",
            .
        @Elixirs
            ElixirOfStrength strength_boost="5" desc="An elixir that boosts strength",
            GreaterElixirOfStrength strength_boost="10" desc="An elixir that greatly boosts strength",
            ElixirOfAgility agility_boost="5" desc="An elixir that boosts agility",
            GreaterElixirOfAgility agility_boost="10" desc="An elixir that greatly boosts agility",
            .
        .
};

println!("{}", loot);
ROOT
 ├─ test{}
 ├─ Armors
 │  ├─ Shirt{defense="0",desc="A simple shirt"}
 │  ├─ HeavyArmor
 │  │  └─ PlateArmor{agility="-2",defense="10",desc="Heavy armor made of plates with decreased agility"}
 │  │     FullPlateArmor{agility="-5",defense="15",desc="Very heavy armor made of plates with greatly decreased agility"}
 │  └─ LightArmor
 │     └─ LeatherArmor{defense="5",desc="Armor made of leather with increased agility",agility="2"}
 │        Chainmail{agility="1",defense="8",desc="Armor made of interlocking rings with moderate agility"}
 ├─ Consumables
 │  ├─ Water{desc="Just water",healing="2"}
 │  ├─ Elixirs
 │  │  └─ ElixirOfStrength{strength_boost="5",desc="An elixir that boosts strength"}
 │  │     GreaterElixirOfStrength{strength_boost="10",desc="An elixir that greatly boosts strength"}
 │  │     ElixirOfAgility{agility_boost="5",desc="An elixir that boosts agility"}
 │  │     GreaterElixirOfAgility{desc="An elixir that greatly boosts agility",agility_boost="10"}
 │  └─ Potion
 │     └─ HealthPotion{desc="A potion that restores a small amount of health",healing="20"}
 │        GreaterHealthPotion{desc="A potion that restores a moderate amount of health",healing="40"}
 │        ManaPotion{mana_restoration="20",desc="A potion that restores a small amount of mana"}
 │        GreaterManaPotion{desc="A potion that restores a moderate amount of mana",mana_restoration="40"}
 └─ Weapons
    ├─ Knife{desc="A simple knife",attack="1"}
    ├─ Axes
    │  └─ BattleAxe{attack="12",critical="8",desc="A battle axe with increased critical hit rate"}
    │     WarAxe{attack="14",desc="A war axe with a high critical hit rate",critical="9"}
    ├─ Mace
    │  └─ MorningStar{attack="13",critical="7",desc="A mace with increased critical hit rate"}
    │     Flail{desc="A flail with a very high critical hit rate",attack="16",critical="11"}
    ├─ Swords
    │  └─ ShortSword{critical="5",desc="A short sword with increased critical hit rate",attack="10"}
    │     LongSword{desc="A long sword with a high critical hit rate",attack="15",critical="10"}
    │     TwoHandedSword{attack="20",desc="A two-handed sword with a very high critical hit rate",critical="15"}
    └─ Wooden
       ├─ BarkShield{attack="0",magic_power="10",desc="A wooden shield reinforced with bark, providing magic power"}
       ├─ Bows
       │  └─ ShortBow{accuracy="10",attack="10",desc="A short bow with high accuracy"}
       │     LongBow{desc="A long bow with ice elemental damage",attack="20",ice_damage="10",accuracy="20"}
       └─ Staffs
          └─ WoodenStaff{desc="A wooden staff imbued with magic power",magic_power="10",attack="5"}
             CrystalStaff{ice_damage="10",desc="A crystal staff with ice elemental damage",magic_power="15",attack="8"}
             ElementalStaff{thunder_damage="15",desc="An elemental staff with thunder elemental damage",magic_power="20",attack="12"}

Tests

cargo test

Bump version

cargo bump minor

Dependencies

~1.2–2MB
~35K SLoC