Skip to content
Cataclysm: Bright Nights
GitHubDiscord

Tilesets

A tileset provides graphic images for the game. Each tileset has one or more tilesheets of image sprites and a tile_config.json file that describes how to map the contents of the sprite sheets to various entities in the game. It also has a tileset.txt file that provides metadata.

Compositing Tilesets

Prior October 2019, tilesets had to be submitted to the repo with each tilesheet fully composited and the sprite indices in tile_config.json calculated by hand. After October 2019, tilesets can be submitted to repos as directories of individual sprite files and tile entry JSON files that used sprite file names, and a Python script that runs at compile time would merge the sprite images into tilesheets, convert the files names into sprite indices for the tile entries, and merge the tile entries into a tile_config.json.

For the rest of this document, tilesets that are submitted as fully composited tilesheets are called legacy tilesets, and tilesets that submitted as individual sprite image files are compositing tilesets.

tools/gfx_tools/decompose.py

This is a Python script that will convert a legacy tileset into a compositing tileset. It reads the tile_config.json and assigns semi-arbitrary file names to each sprite index. Then it changes all the sprite indexes references to the file names. Then it breaks up tile_config.json into many small tile_entry JSON files with arbitrary file names, and pulls out each sprite and writes it to aseparate file.

It requires pyvips to do the image processing.

It takes a single mandatory argument, which is the path to the tileset directory. For example: python tools/gfx_tools/decompose.py gfx/ChestHole16Tileset will convert the legacy ChestHole16 tileset to a compositing tileset.

decompose.py creates a sufficient directory hierarchy and file names for a tileset to be compositing, but it is machine generated and badly organized. New compositing tilesets should use more sensible file names and a better organization.

It shouldn’t be necessary to run decompose.py very often. Legacy tilesets should only need to be converted to composite tilesets one time.

tools/gfx_tools/compose.py

This is a Python script that creates the tilesheets for a compositing tileset. It reads all of the directories in a tileset’s directory with names that start with pngs_ for sprite files and tile_entry JSON files, creates mappings of sprite file names to indices, merges the sprite files into tilesheets, changes all of the sprite file name references in the tile_entries to indices, and merges the tile_entries into a tile_config.json.

Like decompose.py, it requires pyvips to the image processing.

The original sprite files and tile_entry JSON files are preserved.

directory structure

Each compositing tileset has one or more directories in it with a name that starts with pngs_, such as pngs_tree_32x40 or pngs_overlay. These are the image directories. All of the sprites in an image directory must have the same height and width and will be merged into a single tilesheet.

It is recommended that tileset developers include the sprite dimensions in the image directory name, but this is not required. pngs_overlay_24x24 is preferred over pngs_overlay but both are allowed. As each image directory creates its own tilesheet, and tilesheets should be as large as possible for performance reasons, tileset developers are strongly encouraged to minimize the number of image directories.

Each image directory contains a hierarchy of subdirectories, tile_entry JSON files, and sprite files. There is no restriction on the arrangement or names of these files, except for tile_entry JSON files for expansion tilesheets must be at the top level of the image directory. Subdirectories are not required but are recommended to keep things manageable.

tile_entry JSON

Each tile_entry JSON is a dictionary that describes how to map one or more game entities to one or more sprites. The simplest version has a single game entity, a single foreground sprite, an optional background sprite, and a rotation value. For instance:

{                                           // this is an object and doesn't require a list
    "id": "mon_cat",                        // the game entity represented by this sprite
    "fg": "mon_cat_black",                  // some sprite name
    "bg": "shadow_bg_1",                    // some sprite name; always a single value
    "rotates": false                        // true for things that rotate like vehicle parts
}

The values in "id", "fg", and "bg" can be repeated within an image directory or in different image directories. "fg" and "bg" sprite images can be referenced across image directories, but the sprites must be stored in an image directory with other sprites of the same height and width.

"id" can also be a list of multiple game entities sharing the same sprite, like "id": ["vp_door"], ["vp_hddoor"]. "id" can be any vehicle part, terrain, furniture, item, or monster in the game. The special ids "player_female", "player_male", "npc_female", "npc_male" are used to identify the sprites for the player avatar and NPCs. The special id "unknown" provides a sprite that is displayed when an entity has no other sprite.

The special suffixes _season_spring, _season_summer, _season_autumn, and _season_winter can be applied to any entity id to create a seasonal variant for that entity that will be displayed in the appropriate season like this "id": "mon_wolf_season_winter".

The special prefixes overlay_mutation_, overlay_female_mutation_, overlay_male_mutation_ can prefix any trait or bionic in the game to specify an overlay image that will be laid over the player and NPC sprites to indicate they have that mutation or bionic.

The special prefixes overlay_worn_, overlay_female_worn_, overlay_male_worn_ can prefix any item in the game to specify an overlay image that will be laid over the player and NPC sprites to indicate they are wearing that item.

The special prefixes overlay_wielded_, overlay_female_wielded_, overlay_male_wielded_ can prefix any item in the game to specify an overlay image that will be laid over the player and NPC sprites to indicate they are holding that item.

"fg" and "bg" can also be a list of 2 or 4 pre-rotated rotational variants, like "bg": ["t_wall_n", "t_wall_e", "t_wall_s", "t_wall_w"] or "fg": ["mon_dog_left", "mon_dog_right"].

"fg" and "bg" can also be a list of dictionaries of weighted, randomly chosen options, any of which can also be a rotated list:

"fg": [
    { "weight": 50, "sprite": "t_dirt_brown"},       // appears in 50 of 53 tiles
    { "weight": 1, "sprite": "t_dirt_black_specks"}, // appears 1 in 53 tiles
    { "weight": 1, "sprite": "t_dirt_specks_gray"},
    { "weight": 1, "sprite": "t_patchy_grass"}       // file names are arbitrary
],

"multitle" is an optional field. If it is present and true, there must be an additional_tiles list with 1 or more dictionaries for entities and sprites associated with this tile, such as broken versions of an item or wall connections. Each dictionary in the list has an "id” field, as above, and a "fg" field, which can be a single filename, a list of filenames, or a list of dictionaries as above.

Each tile_entry.json file can have a single object in it, or a list of 1 or more objects like so:

[
    { "id": "mon_zombie", "fg": "mon_zombie", "bg": "mon_zombie_bg", "rotates": false },
    { "id": "corpse_mon_zombie", "fg": "mon_zombie_corpse", "bg": "mon_zombie_bg", "rotates": false },
    { "id": "overlay_wielding_corse_mon_zombie", "fg": "wielded_mon_zombie_corpse", "bg": [], "rotates": false }
]

Having a list of tile entries in a file may be useful for organization, but completely unrelated entries may all exist in the same file without any complications.

expansion tile_entry JSON

Tilesheets can have expansion tilesheets, which are tilesheets from mods. Each expansion tilesheet is a single "id" value, "rotates": false", and "fg": 0. Expansion tile_entry JSON are the only tile_entry JSONs that use an integer value for "fg" and that value must be 0. Expansion tile_entry JSONs must be located at the top layer of each image directory.

Sprite Images

Every sprite inside an image directory must have the same height and width as every other sprite in the image directory.

Sprites can be organized into subdirectories within the image directory however the tileset developer prefers. Sprite filenames are completely arbitrary and should be chosen using a scheme that makes sense to the tileset developer.

After loading a tileset, config/debug.log will contain a space separated list of every entity missing a sprite in the tileset. Entities that have sprites because of a "looks_like" definition will not show up in the list.

tile_info.json

Each compositing tileset must have a tile_info.json, laid out like so:

[
  {
    "width": 32,
    "pixelscale": 1,
    "height": 32
  },
  {
    "1_tiles_32x32_0-5199.png": {}
  },
  {
    "2_expan_32x32_5200-5391.png": {}
  },
  {
    "3_tree_64x80_5392-5471.png": {
      "sprite_offset_x": -16,
      "sprite_offset_y": -48,
      "sprite_height": 80,
      "sprite_width": 64
    }
  },
  {
    "4_fallback_5472-9567.png": { "fallback": true }
  }
]

The first dictionary is mandatory, and gives the default sprite width and sprite height for all tilesheets in the tileset. Each of the image directories must have a separate dictionary, containing the tilesheet png name as its key. If the tilesheet has the default sprite dimensions and no special offsets, it can have an empty dictionary as the value for the tilesheet name key. Otherwise, it should have a dictionary of the sprite offsets, height, and width.

A special key is "fallback" which should be true if present. If a tilesheet is designated as fallback, it will be treated as a tilesheet of fallback ASCII characters. compose.py will also compose the fallback tilesheet to the end of the tileset, and will add a “fallback.png” to tile_config.json if there is no "fallback" entry in tile_info.json.

A special is "filler" which should be true if present. If a tilesheet is designated as filler, entries from its directory will be ignored if an entry from a non-filler directory has already defined the same id. Entries will also be ignored if the id was already defined by in the filler directory. Also, pngs from a filler directory will be ignored if they share a name with a png from a non-filler directory. A filler tilesheet is useful when upgrading the art in a tileset: old, low-quality art can be placed on filler tilesheet and will be automatically replaced as better images are added to the non-filler tilesheets.

Legacy tilesets

tilesheets

Each tilesheet contains 1 or more sprites with the same width and height. Each tilesheet contains one or more rows of exactly 16 sprites. Sprite index 0 is special and the first sprite of the first tilesheet in a tileset should be blank. Indices run sequentially through each sheet and continue incrementing for each new sheet without reseting, so index 32 is the first sprite in the third row of the first sheet. If the first sheet has 320 sprites in it, index 352 would be the first sprite of the third row of the second sheet.

tile_config

Each legacy tileset has a tile_config.json describing how to map the contents of a sprite sheet to various tile identifiers, different orientations, etc. The ordering of the overlays used for displaying mutations can be controlled as well. The ordering can be used to override the default ordering provided in mutation_ordering.json. Example:

{ // whole file is a single object
  "tile_info": [ // tile_info is mandatory
    {
      "height": 32,
      "width": 32,
      "iso": true, //  Optional. Indicates an isometric tileset. Defaults to false.
      "pixelscale": 2 //  Optional. Sets a multiplier for resizing a tileset. Defaults to 1.
    }
  ],
  "tiles-new": [ // tiles-new is an array of sprite sheets
    { //   alternately, just one "tiles" array
      "file": "tiles.png", // file containing sprites in a grid
      "tiles": [ // array with one entry per tile
        {
          "id": "10mm", // id is how the game maps things to sprites
          "fg": 1, //   lack of prefix mostly indicates items
          "bg": 632, // fg and bg can be sprite indexes in the image
          "rotates": false
        },
        {
          "id": "t_wall", // "t_" indicates terrain
          "fg": [2918, 2919, 2918, 2919], // 2 or 4 sprite numbers indicates pre-rotated
          "bg": 633,
          "rotates": true,
          "multitile": true,
          "additional_tiles": [ // connected/combined versions of sprite
            { //   or variations, see below
              "id": "center",
              "fg": [2919, 2918, 2919, 2918]
            },
            {
              "id": "corner",
              "fg": [2924, 2922, 2922, 2923]
            },
            {
              "id": "end_piece",
              "fg": [2918, 2919, 2918, 2919]
            },
            {
              "id": "t_connection",
              "fg": [2919, 2918, 2919, 2918]
            },
            {
              "id": "unconnected",
              "fg": 2235
            }
          ]
        },
        {
          "id": "vp_atomic_lamp", // "vp_" vehicle part
          "fg": 3019,
          "bg": 632,
          "rotates": false,
          "multitile": true,
          "additional_tiles": [
            {
              "id": "broken", // variant sprite
              "fg": 3021
            }
          ]
        },
        {
          "id": "t_dirt",
          "rotates": false,
          "fg": [
            { "weight": 50, "sprite": 640 }, // weighted random variants
            { "weight": 1, "sprite": 3620 },
            { "weight": 1, "sprite": 3621 },
            { "weight": 1, "sprite": 3622 }
          ]
        },
        {
          "id": [
            "overlay_mutation_GOURMAND", // character overlay for mutation
            "overlay_mutation_male_GOURMAND", // overlay for specified gender
            "overlay_mutation_active_GOURMAND" // overlay for activated mutation
          ],
          "fg": 4040
        }
      ]
    },
    { // second entry in tiles-new
      "file": "moretiles.png", // another sprite sheet
      "tiles": [
        {
          "id": ["xxx", "yyy"], // define two ids at once
          "fg": 1,
          "bg": 234
        }
      ]
    }
  ],
  "overlay_ordering": [
    {
      "id": "WINGS_BAT", // mutation name, in a string or array of strings
      "order": 1000 // range from 0 - 9999, 9999 being the topmost layer
    },
    {
      "id": ["PLANTSKIN", "BARK"], // mutation name, in a string or array of strings
      "order": 3500 // order is applied to all items in the array
    },
    {
      "id": "bio_armor_torso", // Overlay order of bionics is controlled in the same way
      "order": 500
    }
  ]
}