LootLib

by Ter13
Abstract loot generators powered by JSON. [More]
To download this library for your Linux/Mac installation, enter this on your command line:

DreamDownload byond://Ter13.LootLib##version=1

Emulator users, in the BYOND pager go to File | Open Location and enter this URL:

byond://Ter13.LootLib##version=1

17 downloads
Version 1.0a
Date added: Jun 11 2023
Last updated: Sep 25 2023
1 fan
LootLib is a simple, but powerful library that allows you to generate reusable randomized tables that can generate any kind of datum.

Loot tables are defined in JSON, and parsed into a data structure that can be passed to the generate_loot() global command, or indexed in global.generator_cache by name. Loot generators can also reference one another by name from a local or global generator cache to greatly simplify the creation of complex loot tables.

Check out the documentation for more information.

Comments

Ter13: (Jun 12 2023, 9:10 pm)
Multiple items can be generated with weightless nodes:

[
    {"type":"/obj/item/meat","prob":80,"roll":"1d2"},
    {"type":"/obj/item/hide","prob":50,"amt":2},
    {"type":"/obj/item/bones","prob":90,"range":"2 to 3"},
    {"type":"/obj/item/sinew","prob":5}
]


The above generator has a chance to drop everything in it. This is because it has no weighting. Instead, it uses probabilities. An item will always drop if it has no weight, the item before it in the list has tried to drop, and its probability passes.

Let's say we want to always drop meat, but only drop one of a skull, sinew, or a horn:

[
    {"type":"/obj/item/meat","roll":"1d2"},
    {"type":"/obj/item/skull","weight":1},
    {"type":"/obj/item/sinew","weight":1},
    {"type":"/obj/item/horn","weight":1}
]


In this case, because meat has no weight, and it comes before weighted items, it will always generate. Only one of the weighted items will generate. In this case, because the total weight of this array is 3, the chance for each of these weighted outcomes is 1 in 3.

However, if we were to move meat from the front of the list to the back of the list:

[
    {"type":"/obj/item/skull","weight":1},
    {"type":"/obj/item/sinew","weight":1},
    {"type":"/obj/item/horn","weight":1},
    {"type":"/obj/item/meat","roll":"1d2"}
]


This version of the generator would only generate meat if the horn was picked. If we were to move it into the middle of the list:

[
    {"type":"/obj/item/skull","weight":1},
    {"type":"/obj/item/sinew","weight":1},
    {"type":"/obj/item/meat","roll":"1d2"},
    {"type":"/obj/item/horn","weight":1}
]


In this case, it would only generate if the sinew was generated. So your loot would either be a 1) skull, 2) a horn, or 3) a sinew and 1 to 2 peices of meat.


Now let's say we only want horns, sinew, and meat to generate 10% of the time:

[
    {"type":"/obj/item/meat","roll":"1d2"},
    {"type":"/obj/item/skull","weight":1,"prob":10},
    {"type":"/obj/item/sinew","weight":1,"prob":10},
    {"type":"/obj/item/horn","weight":1,"prob":10}
]


[
    {"type":"/obj/item/meat","roll":"1d2"},
    {"table":[
        {"type":"/obj/item/skull","weight":1},
        {"type":"/obj/item/sinew","weight":1},
        {"type":"/obj/item/horn","weight":1}
    ],"prob":10}
]


We can add a probability to each item, OR, we can add an unweighted table with a 10% probability. The outcome will be the same, however, the table method will not generate a random number between 1 and 3 unless the prob(10) roll succeds, whereas the inline method will rand(1,3) first, pick and item, and then check if prob(10) succeeds, so their impact on random number generation will differ slightly.

With the table argument, you can pass an id instead of an array of nodes. The id will be looked up in the generator_cache passed to the generate_loot function first, and if not found, will be looked up in the global.generator_cache list. If not found at all, it will simply be skipped.

So let's say we wanted every animal to have similar rules to how it drops loot, but vary in how it works. We could use local table ids for this:

creatures table:
[
    {"table":"flesh"},
    {"table":"rare_products","prob":10}
]


This generator would just be the structure for setting up the final generator. We'd pass it to generate_loot(), which would then use additional generators you pass into the generator cache argument:

flesh table:
[
    {"type":"/obj/item/meat","roll":"1d3"}
    {"type":"/obj/item/hide","prob":80}
]


rare_products table:
[
    {"weight":1,"type":"/obj/item/skull"}
    {"weight":1,"type":"/obj/item/horn"}
    {"weight":1,"type":"/obj/item/sinew"}
]


If we passed references to these loot tables in with a cache, we could use the creatures table as a basis, and then have each monster have its own flesh and rare_products table reference. When we kill that monster, we call

var/c = global.generator_cache
generate_loot(c["creature table"],list("flesh"=c[src.flesh_loot],"rare_products"=c[src.rare_loot]))


The seed table can now be pointed to the local cache we just built, and is now a complete loot table combining both rule sets.
Ter13: (Jun 12 2023, 8:21 pm)
roll_range() proc

  Arguments:
    roll - a range string or a range list

  Returns:
    A random integer between L and H.

  This proc takes a list in the format: list(L,H), where L is the lower integer and H is the upper integer of a range value. It can also take a string "L to H", where L and H are the same integer components of the range. Invalid range values throw an "invalid range" exception.



loot_generator() proc

  Arguments:
    json a JSON string containing an array of nodes to process into a generator

  Returns:
    A processed generator, represented as a list of lists.

  Passing this proc a JSON array containing objects representing nodes will process the array and nodes, validating them. Any invalid records will be removed from the final product. In order to be valid, a node must have a "type" or "table" argument at minimum.

  Node structure:
    weight - num (optional)
    prob - num [0 to 100] (optional)
    One of:
      type - a path string (optional)
      table - a generator id string, or a list of nodes (optional)
    One of:
      amt - num
      roll - a dice string
      range - a range string

  Processed nodes:

    generate - ["type" | "table"]
    rand - ["amt" | "roll" | "range"]
    total - helps the generator pick a value from the table using weights

    Once a node is processed, a few properties are added to help the generator handle it. Excess properties are removed. If an "amt" property is present, a "roll" or "range" property will be discarded, for instance. If a "type" property is present, the "table" property is discarded. If both type and table properties are missing, the node will be discarded.

  Weighting
    The total weight for each array of nodes is added up during processing. The weight of all prior nodes is added to the total of that node. Nodes with a weight of 0 have special behavior during generation.



generate_loot() proc

  Arguments:
    generator - a processed loot generator as built by loot_generator()
    generator_cache - an associative list of named generators for table references (optional)

  Returns:
    a list of generated datums, or an empty list if nothing was generated

  • The loot generator will iterate over the entire list, checking each node:
    • If the node has a weight, and no roll has been made for this array, the total of the last item in the list will be multiplied against a random value between 0 to 1. This roll will then be used to find valid results.
    • If the node does not have a weight, and the current roll is less than or equal the total of the node, the node will be generated.
    • If the node does have a weight, and the current roll is less than or equal the total of the node, and a weighted node has been generated already, we will halt looping over this node list.
    • When a node is being generated, its prob property is checked. If it doesn't have one, or a a prob() check on the property passes, the item is generated. If the check fails, the node won't be generated.
    • When a node is being generated, its randomization type is checked. amt will be looked up, and range or roll will be rolled to give us how many times this node will be generated.
    • If the node's generate property is "type", the type path referenced will be created and onGenerated() will be called on the newly created object, passing a pointer to the value that stores how many times we're going to try to generate the node. onGenerated() decreases the value in this pointer by 1 by default. If you have a stacking system, you can deduct your stacked items properly using this pointer so that you don't have to generate 100 objects and then destroy 99 when they get stacked up on each other.
    • If the node's generate property is "table", the generator_cache is checked for the string, and if not found, global.generator_cache is checked for the string to get a list of generator nodes. table can also be a list of generator nodes itself. In either case, if the list is valid, it is added to the processing queue n times, according to the "rand" property we generated above.
  • The loot generator will also process any enqueued generator lists, and when the queue is empty, return all the objects generated.




datum.onGenerated() proc

  Arguments:
    &amt - a pointer to a counter that is used to generate multiples of this item.

  Returns:
    src

  This is called when an object is generated by a loot generator. By default, this action subtracts one from the value of the amt pointer that is passed from the generate_loot proc that created this object. Overriding this allows you to reduce this number by more, or to return an entirely different object, or to do additional setup work on the object.



global.generator_cache list

  This variable is used by loot generators as a fallback lookup for cached generators. Storing a processed loot generator by an id allows you to reference the id in the table property of a generator node, allowing loot generators to reference and compound one another. When passing a local cache to a generate_loot call, it's possible to refer to a global loot generator by a different name locally. Take advantage of this to reduce the complexity and memory impact of your loot generators. Loot generators are capable of very complex sets of rules without themselves having to be particularly large, provided they are constructed from a series of cached generators instead of repeated throughout your cache.