Race File Specifications - Grisgram/gml-raptor GitHub Wiki

Race files are normal json files containing the properties of items to drop and any additional information you want to store as custom attributes.

First, lets look at a "standard" race table:

{
    "demotable": {
        "loot_count": 1,
        "items" : {
            "object1":    {"type" : "TestDrop1_1", "always" : 0, "unique" : 1, "enabled" : 1, "chance" : 10.25  },
            "DemoItem1":  {"type" : "TestDrop2_2", "always" : 0, "unique" : 0, "enabled" : 1, "chance" : 50.0   },
            "DemoValue1": {"type" : "TestDrop3_3", "always" : 0, "unique" : 1, "enabled" : 1, "chance" : 100.0  },
        },
    },
}

demotable is the name of this race table.
It contains the table property loot_count which defines the number of items to drop when this table is queried.

items is the list of items this table contains (the things that can "drop" in game). Each of them has a name ("object1", "DemoItem1", etc...) and also contains the standard race properties type, always, unique, enabled and chance.

However, the most important value here is type.
This field holds the name of the object in the asset browser (the object name) that can drop.
The table will call instance_create_layer() with this name to create an instance of the object when it drops. You must supply a layer_name_or_depth as an argument to a call, to tell race, where to drop the item/object.
More information about that can be found in the Race Functions page.

Custom named attributes

Race supports adding custom attributes to each item in a table.
This struct must be named attributes. You may put any data you need into this.
You can assign any value or datatype to an attribute, even methods (at runtime)!

Here is an example of what it looks like to create custom attributes in the json file:

    "object1": {"type": "TestDrop1_1", "always": 0, "unique": 1, "enabled": 1, "chance": 10.25,	
        "attributes" : {
            "gold_value" : 105.00,
            "min_level" : 25,
        },
    },

In this item, two custom attributes are added: the gold_value and a min_level.
As it is just a struct, you can access it at runtime through item.attributes.xxx, as you would access any other struct member in any other object.
The item filter function also offers a .for_attribute(...) filter.

Null drops (no drop)

To create a variable amount of items dropped by a query, you can add a null drop value to the table. This is a special item, that will drop "nothing". Based on the chance and uniqueness of such a null drop, you can create queries that drop, for example, "up-to-four items".

To create a null drop, set the type of the item to null (in json) or to undefined (in GML).
If you create your table in code, there is a RaceNullItem(...) class available, which creates such an entry for you.

Tip

You can add multiple null drops to your table to be able to create not only a result set of "up-to-4" items, but even result sets of "2-4 items"!
Set the loot_count = 4 and add two null drops, make each of them unique = 1 and there can be 0-2 null drops, reducing the result set to 2-4.
Your combination options are endless with Race. It is as powerful as your creativity, when you design your tables.

Sub References (Table recursion)

To understand how recursion is implemented in the json structs, let's look at a standard item first:

    "CoolWeapon": {"type": "WeaponObject", "always": 0, "unique": 1, "enabled": 1, "chance": 36.0 },

The type is WeaponObject.
This will result in a call to

instance_create(x, y, _layer_name_or_depth_, asset_get_index("WeaponObject"));

when it gets hit by a query.

Now, instead of a single item (WeaponObject), we can add entire tables as an entry to a table.
When a query finds such a subtable, it begins recursively querying that subtable. If another subsubtable is found, the recursion continues until finally a single item is found.

There are two ways to add subtables to a table: by_reference or as a copy.
The difference is in the properties of the added tables and all properties of items in the subtable (through all recursion levels!).

If you add a subtable by_reference, it's just a pointer to the other table. All referenced tables share the same properties for type, always, enabled, unique and chance. If you change one of them, you change it for ALL referenced tables.

If you add a table as a copy, the entire structure of this table (including all recursive subtables, a so-called deep copy) is copied to a dynamically created table. All properties are copied as well, meaning, you can adapt each single item in this structure without affecting all other copies of this table and its subs.

Add by_reference

To add a subtable by_reference, use an equals sign (=) together with the referenced table name in the type field like this:

    "Weapons": {"type": "=weapons_table", "always": 0, "unique": 1, "enabled": 1, "chance": 36.0 },

When a query finds this entry, the query simply continues in the weapons_table, using the items and properties found there.

Add as_a_copy

To add a subtable as a copy, use a plus sign (+) together with the table name to copy in the type field like this:

    "Weapons": {"type": "+weapons_table", "always": 0, "unique": 1, "enabled": 1, "chance": 36.0 },

NOTE: By default, Copy-References are resolved on the first encounter of such an entry. Very deep structures can have a minor performance impact on the frame, where the deep copy takes place. However, they are resolved only once with the values, the copied table has, when the copy is performed.

A real example

To make things more clear, I want to show you a real-life example of one of the race files I used in the game One (K)night alone which I did for a game jam in 2022. This was the file that created all the dungeon levels, chests, barrels, monsters and even decided how much weapon-bonus you get when you use an anvil and even "dropped" the string to use from LG for what the hero would say when the anvil is used!

Take a few minutes and look closely at the file - I hope it will inspire you to see what can be done with race quite easily. In the jam game, there is no complicated code for generating the random levels. It's just setting the loot_count to the number of fields to generate based on level size, followed by a call to the query function.

This is the source code of One (K)niht alone that creates all dungeon levels. No kidding. That's it...

	var tbl = race.reset_table("fields");
	// (x * y - 1) because field(0,0) is always empty (hero starts there)
	race.tables.fields.fields.loot_count = sizex * sizey - 1;
	var result = race.tables.fields.query(LAYER_FIELDS);

Note

Yes, those three lines of code generate the entire random dungeon!

You can expand the full file below, but let's take a closer look at the entry table of such a random dungeon query, where you can see, how you can set up things like this:

{
    "map_fields": {
        "loot_count": 1,
        "items": {
            "exit":     {"type": "ExitField",        "always": 1, "unique": 1, "enabled": 1, "chance": 1.0,  "attributes": { "type": "LevelExit" } },
            "anvil":    {"type": "AnvilField",       "always": 1, "unique": 1, "enabled": 1, "chance": 1.0,  "attributes": { "type": "Anvil" } },
            "empty":    {"type": "EmptyField",       "always": 1, "unique": 0, "enabled": 1, "chance": 30.0 },
            "monster":  {"type": "=monster_spawns",  "always": 1, "unique": 0, "enabled": 1, "chance": 35.0 },
            "trap":     {"type": "=trap_spawns",     "always": 0, "unique": 0, "enabled": 1, "chance": 10.0 },
            "treasure": {"type": "=treasure_spawns", "always": 1, "unique": 0, "enabled": 1, "chance": 25.0 },
        },
    },

You can see here:

  • There is only one entry for each "type of field" in the dungeon
  • Except for the trap, all items are set to always = 1, which means, each dungeon will contain at least one of each type
  • ExitField and AnvilField (the repair station) are in addition set to unique = 1, so each level will contain exactly one exit field and one repair station
  • Then, instead of adding all possible enemies, traps and spawns to this table (which would end up in a huge load of chance values to balance), I simply added three reference tables, one for each spawn (monsters, traps and treasures)
  • This way, it was easy to set and balance chances for the level: mostly it will be monsters (chance 35), followed by treasures (chance 25) and finally traps (chance 10) have the least chance of appearing, as those are quite deadly elements in the dungeon
Click to expand the full data file!
{
    "map_fields": {
        "loot_count": 1,
        "items": {
            "exit":     {"type": "ExitField",        "always": 1, "unique": 1, "enabled": 1, "chance": 1.0,  "attributes": { "type": "LevelExit" } },
            "anvil":    {"type": "AnvilField",       "always": 1, "unique": 1, "enabled": 1, "chance": 1.0,  "attributes": { "type": "Anvil" } },
            "empty":    {"type": "EmptyField",       "always": 1, "unique": 0, "enabled": 1, "chance": 30.0 },
            "monster":  {"type": "=monster_spawns",  "always": 1, "unique": 0, "enabled": 1, "chance": 35.0 },
            "trap":     {"type": "=trap_spawns",     "always": 0, "unique": 0, "enabled": 1, "chance": 10.0 },
            "treasure": {"type": "=treasure_spawns", "always": 1, "unique": 0, "enabled": 1, "chance": 25.0 },
        },
    },
    "monster_spawns": {
        "loot_count": 1,
        "items": {
            "vampire":  {"type": "MonsterField", "always": 0, "unique": 0, "enabled": 1, "chance": 10.0, "attributes": { "type": "Vampire" } },
            "bat":      {"type": "MonsterField", "always": 0, "unique": 0, "enabled": 1, "chance": 10.0, "attributes": { "type": "Bat" } },
            "skeleton": {"type": "MonsterField", "always": 0, "unique": 0, "enabled": 1, "chance": 10.0, "attributes": { "type": "Skeleton" } },
            "zombie":   {"type": "MonsterField", "always": 0, "unique": 0, "enabled": 1, "chance": 10.0, "attributes": { "type": "Zombie" } },
        },
    },
    "trap_spawns": {
        "loot_count": 1,
        "items": {
            "single": {"type": "TrapSingleField", "always": 0, "unique": 0, "enabled": 1, "chance": 75.0, "attributes": { "type": "SingleTrap" } },
            "double": {"type": "TrapDoubleField", "always": 0, "unique": 0, "enabled": 1, "chance": 25.0, "attributes": { "type": "DoubleTrap" } },
        },
    },
    "treasure_spawns": {
        "loot_count": 1,
        "items": {
            "barrel":       {"type": "TreasureField", "always": 0, "unique": 0, "enabled": 1, "chance": 10.0, "attributes": { "type": "Barrel",     "contents":"barrel_contents" } },
            "silver_chest": {"type": "TreasureField", "always": 0, "unique": 0, "enabled": 1, "chance": 10.0, "attributes": { "type": "SilverChest","contents":"silver_contents" } },
            "gold_chest":   {"type": "TreasureField", "always": 0, "unique": 0, "enabled": 1, "chance":  5.0, "attributes": { "type": "GoldChest",  "contents":"gold_contents" } },
        },
    },
    
    
    "barrel_contents": {
        "loot_count": 1,
        "items": {
            "empty":         {"type": "<null>",       "always": 0, "unique": 1, "enabled": 1, "chance": 50.0 },
            "health_small":  {"type": "HealthPotion", "always": 0, "unique": 1, "enabled": 1, "chance": 15.0, "attributes": { "value": 20 } },
            "shield_small":  {"type": "ShieldBuff",   "always": 0, "unique": 1, "enabled": 1, "chance": 15.0, "attributes": { "value": 20 } },
            "weapon_small":  {"type": "WeaponBuff",   "always": 0, "unique": 1, "enabled": 1, "chance": 15.0, "attributes": { "value": 20 } },
            "xp_small":      {"type": "XPBuff",       "always": 0, "unique": 1, "enabled": 1, "chance": 20.0, "attributes": { "value": 10 } },
            "health_medium": {"type": "HealthPotion", "always": 0, "unique": 1, "enabled": 1, "chance":  2.0, "attributes": { "value": 40 } },
            "shield_medium": {"type": "ShieldBuff",   "always": 0, "unique": 1, "enabled": 1, "chance":  2.0, "attributes": { "value": 40 } },
            "weapon_medium": {"type": "WeaponBuff",   "always": 0, "unique": 1, "enabled": 1, "chance":  2.0, "attributes": { "value": 40 } },
            "xp_medium":     {"type": "XPBuff",       "always": 0, "unique": 1, "enabled": 1, "chance":  3.0, "attributes": { "value": 20 } },
        },
    },
    "silver_contents": {
        "loot_count": 3,
        "items": {
            "empty":         {"type": "<null>",       "always": 0, "unique": 1, "enabled": 1, "chance": 40.0 },
            "empty2":        {"type": "<null>",       "always": 0, "unique": 1, "enabled": 1, "chance": 10.0 },
            "health_small":  {"type": "HealthPotion", "always": 0, "unique": 1, "enabled": 1, "chance":  5.0, "attributes": { "value": 20 } },
            "shield_small":  {"type": "ShieldBuff",   "always": 0, "unique": 1, "enabled": 1, "chance":  5.0, "attributes": { "value": 20 } },
            "weapon_small":  {"type": "WeaponBuff",   "always": 0, "unique": 1, "enabled": 1, "chance":  5.0, "attributes": { "value": 20 } },
            "xp_small":      {"type": "XPBuff",       "always": 0, "unique": 1, "enabled": 1, "chance": 10.0, "attributes": { "value": 10 } },
            "health_medium": {"type": "HealthPotion", "always": 0, "unique": 1, "enabled": 1, "chance": 20.0, "attributes": { "value": 40 } },
            "shield_medium": {"type": "ShieldBuff",   "always": 0, "unique": 1, "enabled": 1, "chance": 20.0, "attributes": { "value": 40 } },
            "weapon_medium": {"type": "WeaponBuff",   "always": 0, "unique": 1, "enabled": 1, "chance": 20.0, "attributes": { "value": 40 } },
            "xp_medium":     {"type": "XPBuff",       "always": 0, "unique": 1, "enabled": 1, "chance": 30.0, "attributes": { "value": 20 } },
        },
    },
    "gold_contents": {
        "loot_count": 3,
        "items": {
            "health_medium": {"type": "HealthPotion", "always": 0, "unique": 1, "enabled": 1, "chance":  5.0, "attributes": { "value": 40 } },
            "shield_medium": {"type": "ShieldBuff",   "always": 0, "unique": 1, "enabled": 1, "chance":  5.0, "attributes": { "value": 40 } },
            "weapon_medium": {"type": "WeaponBuff",   "always": 0, "unique": 1, "enabled": 1, "chance":  5.0, "attributes": { "value": 40 } },
            "xp_medium":     {"type": "XPBuff",       "always": 0, "unique": 1, "enabled": 1, "chance": 10.0, "attributes": { "value": 20 } },
            "health_large":  {"type": "HealthPotion", "always": 0, "unique": 1, "enabled": 1, "chance": 25.0, "attributes": { "value": 80 } },
            "shield_large":  {"type": "ShieldBuff",   "always": 0, "unique": 1, "enabled": 1, "chance": 25.0, "attributes": { "value": 80 } },
            "weapon_large":  {"type": "WeaponBuff",   "always": 0, "unique": 1, "enabled": 1, "chance": 30.0, "attributes": { "value": 80 } },
            "xp_large":      {"type": "XPBuff",       "always": 0, "unique": 1, "enabled": 1, "chance": 40.0, "attributes": { "value": 35 } },
        },
    },

    "anvil_repair": {
        "loot_count": 1,
        "items": {
            "repair_1":  {"type": "SwordRepair", "always": 0, "unique": 1, "enabled": 1, "chance":  5.0, "attributes": { "value": 1, "message":"player_strings/anvil_repair_weak" } },
            "repair_2":  {"type": "SwordRepair", "always": 0, "unique": 1, "enabled": 1, "chance": 15.0, "attributes": { "value": 2, "message":"player_strings/anvil_repair_weak" } },
            "repair_3":  {"type": "SwordRepair", "always": 0, "unique": 1, "enabled": 1, "chance": 35.0, "attributes": { "value": 3, "message":"player_strings/anvil_repair_weak" } },
            "repair_4":  {"type": "SwordRepair", "always": 0, "unique": 1, "enabled": 1, "chance": 35.0, "attributes": { "value": 4, "message":"player_strings/anvil_repair_ok" } },
            "repair_5":  {"type": "SwordRepair", "always": 0, "unique": 1, "enabled": 1, "chance": 30.0, "attributes": { "value": 5, "message":"player_strings/anvil_repair_ok" } },
            "repair_6":  {"type": "SwordRepair", "always": 0, "unique": 1, "enabled": 1, "chance": 25.0, "attributes": { "value": 6, "message":"player_strings/anvil_repair_ok" } },
            "repair_7":  {"type": "SwordRepair", "always": 0, "unique": 1, "enabled": 1, "chance": 25.0, "attributes": { "value": 7, "message":"player_strings/anvil_repair_ok" } },
            "repair_8":  {"type": "SwordRepair", "always": 0, "unique": 1, "enabled": 1, "chance": 20.0, "attributes": { "value": 8, "message":"player_strings/anvil_repair_good" } },
            "repair_9":  {"type": "SwordRepair", "always": 0, "unique": 1, "enabled": 1, "chance": 15.0, "attributes": { "value": 9, "message":"player_strings/anvil_repair_good" } },
            "repair_10": {"type": "SwordRepair", "always": 0, "unique": 1, "enabled": 1, "chance":  5.0, "attributes": { "value":10, "message":"player_strings/anvil_repair_good" } },
        },
    },   
}

Continue reading in Race Functions.

⚠️ **GitHub.com Fallback** ⚠️