Defining Bundle Tile Transitions

Define automatic tile transition overlays in Selene bundles with transition priorities and neighbour masks.

Defining Bundle Tile Transitions

Tile transitions let Selene place visual edge and corner overlays automatically where different base tiles meet. They are useful for terrain borders such as grass against dirt, rock against sand, or water against shore tiles.

Transitions live in the common transitions registry. The server resolves them when preparing map chunks for a viewer and sends the resulting transition tiles as additional tile layers. The original base map tiles are not changed.

How transitions work

For each coordinate, Selene looks at the eight neighboring base tiles around the center tile. For each neighboring tile type, it builds a neighbour mask describing which surrounding positions contain that tile. If that tile type has a matching transition definition, Selene can add the referenced transition tile on top of the center coordinate.

The transition definition belongs to the tile that is visually spreading into its neighbors. For example, if myworld:terrain/grass should draw edges over adjacent dirt, define transitions for myworld:terrain/grass, and point each matching pattern at grass edge overlay tiles.

Only one transition overlay is selected per coordinate. If several neighboring tile types match, the highest-priority matching transition wins.

Where transitions live

Put transition definitions under common/data/<namespace>/, where <namespace> becomes the namespace part of each transition entry identifier.

You can define multiple transition entries in one registry file:

my-bundle/
  bundle.json
  common/
    data/
      myworld/
        transitions.json
        tiles.json

Or split each transition entry into its own JSON file:

my-bundle/
  common/
    data/
      myworld/
        transitions/
          terrain/
            grass.json

The transition entry identifier must match the tile identifier that owns the transition rules. This merged file defines transitions for myworld:terrain/grass:

{
  "entries": {
    "terrain/grass": {
      "priority": 20,
      "transitions": []
    }
  }
}

The split-file form uses the file path as the local name. For example, common/data/myworld/transitions/terrain/grass.json also defines transitions for myworld:terrain/grass.

Transition tiles

Transition definitions reference normal tiles from the tiles registry. Define those overlay tiles separately and give them visuals like any other tile:

{
  "entries": {
    "terrain/grass_edge_north": {
      "visual": "myworld:terrain/grass_edge_north",
      "passableAbove": true,
      "tags": ["myworld:transition"]
    },
    "terrain/grass_inner_corner_nw": {
      "visual": "myworld:terrain/grass_inner_corner_nw",
      "passableAbove": true,
      "tags": ["myworld:transition"]
    }
  }
}

Use passableAbove: true when the overlay should not make the coordinate blocked just because it is placed above another tile. This is common for terrain edge graphics.

Transition shape

A transition definition has two fields:

{
  "priority": 20,
  "transitions": [
    {
      "neighbours": [
        "010",
        "0X0",
        "000"
      ],
      "tile": "myworld:terrain/grass_edge_north"
    }
  ]
}

Fields:

  • priority decides which terrain wins when multiple neighboring tile types could place a transition overlay.
  • transitions is an ordered list of mask entries.
  • neighbours is a three-row pattern for the eight surrounding positions.
  • tile is the tile identifier to add as the transition overlay when the pattern matches.

Neighbour masks

The neighbours pattern is read as a 3x3 grid around the center coordinate:

NW N NE
W  X E
SW S SE

Use 1 for positions that must contain the transition owner's base tile. Use 0 for positions that must not contain that base tile. The center character is ignored by the engine; X is the usual convention.

This pattern matches when the owner's base tile is directly north of the center coordinate and not in any other neighboring position:

{
  "neighbours": [
    "010",
    "0X0",
    "000"
  ],
  "tile": "myworld:terrain/grass_edge_north"
}

This pattern matches a north-west corner made from north, north-west, and west neighbors:

{
  "neighbours": [
    "110",
    "1X0",
    "000"
  ],
  "tile": "myworld:terrain/grass_inner_corner_nw"
}

Patterns are exact for the owner tile. A 0 does not mean empty; it means that position is any base tile other than the owner tile.

Priority

Priorities decide which transition overlay gets added when several surrounding tile types match. Selene chooses the matching transition with the highest priority.

The center tile's own transition priority also matters. A neighboring transition is only applied if its priority is greater than the center tile's transition priority. This lets high-priority terrain prevent lower-priority terrain from drawing over it.

For example:

{
  "entries": {
    "terrain/stone_floor": {
      "priority": 1000,
      "transitions": []
    },
    "terrain/grass": {
      "priority": 20,
      "transitions": [
        {
          "neighbours": [
            "010",
            "0X0",
            "000"
          ],
          "tile": "myworld:terrain/grass_edge_north"
        }
      ]
    },
    "terrain/dirt": {
      "priority": 10,
      "transitions": []
    }
  }
}

In this setup, grass can draw over dirt because 20 > 10. Grass does not draw over stone floor because 20 is not greater than 1000.

Use empty transitions arrays for tiles that should participate in priority comparisons but do not place overlays themselves.

Complete example

This example defines a grass transition overlay that appears on a dirt tile when grass is north of it.

my-bundle/
  common/
    data/
      myworld/
        tiles.json
        transitions.json
  client/
    data/
      myworld/
        visuals.json
    textures/
      myworld/
        terrain/
          dirt.png
          grass.png
          grass_edge_north.png

common/data/myworld/tiles.json:

{
  "entries": {
    "terrain/dirt": {
      "visual": "myworld:terrain/dirt"
    },
    "terrain/grass": {
      "visual": "myworld:terrain/grass"
    },
    "terrain/grass_edge_north": {
      "visual": "myworld:terrain/grass_edge_north",
      "passableAbove": true,
      "tags": ["myworld:transition"]
    }
  }
}

common/data/myworld/transitions.json:

{
  "entries": {
    "terrain/dirt": {
      "priority": 10,
      "transitions": []
    },
    "terrain/grass": {
      "priority": 20,
      "transitions": [
        {
          "neighbours": [
            "010",
            "0X0",
            "000"
          ],
          "tile": "myworld:terrain/grass_edge_north"
        }
      ]
    }
  }
}

client/data/myworld/visuals.json:

{
  "entries": {
    "terrain/dirt": {
      "type": "simple",
      "texture": "client/textures/myworld/terrain/dirt.png"
    },
    "terrain/grass": {
      "type": "simple",
      "texture": "client/textures/myworld/terrain/grass.png"
    },
    "terrain/grass_edge_north": {
      "type": "simple",
      "texture": "client/textures/myworld/terrain/grass_edge_north.png"
    }
  }
}

Practical conventions

  • Keep transition overlay tiles in the tiles registry with clear names such as terrain/grass_edge_north or transitions/grass_to_dirt_north.
  • Define visuals for transition tiles in client data, just like base terrain visuals.
  • Give transition overlays passableAbove: true unless they are deliberately meant to affect collision.
  • Use higher priorities for terrain that should visually cover lower-priority terrain.
  • Add empty transition definitions for tiles that should block lower-priority overlays but do not need overlays of their own.
  • Generate large transition sets when your art has many edge and corner combinations, but keep the generated output in the normal registry layout.