02 — Asset & Terrain Framework
A composable system of biome, terrain, object, and gameplay layers. Tile packs are interchangeable. Theme packs re-skin the same map into a completely different visual style. Edge auto-tiling and procedural generation mean the user never has to place every tree.
1. Four layers
A world document composes four independent layers (see also
01-data-model.md §4):
World
├── Biome Layer (canonical, pack-independent) e.g. jungle, desert, ice
├── Terrain Layer (pack-specific) e.g. dense_jungle, oasis
├── Object Layer (pack-specific) e.g. palm_tree, ruin
└── Gameplay Layer (encounters, resources, triggers)
The biome layer is canonical — every pack, theme, and projection must agree on
which biomes exist (see packages/shared/src/biomes.ts). The other three layers are
pack-specific: a Jungle Adventure Pack and a Viking Pack both expose a jungle biome,
but with completely different terrain, objects and gameplay data.
2. Map modes & tile families
The framework supports several distinct map modes. Each mode has its own tile families and rules. A world declares which mode(s) it uses.
2.1 World Map (hex / square, large scale)
Continental-scale maps. One tile ≈ a few kilometres.
Tile families:
- Ocean: ocean, deep_ocean
- Coast: coast, beach
- Grassland: grassland, hills
- Forest: forest, dense_forest, jungle, swamp
- Mountain: hills, mountains, snow_mountains, volcano
- Desert: desert, oasis
- Tundra: tundra, ice
Per-tile attributes:
{
"tile": "dense_jungle",
"movement": 3, // base movement cost (hex)
"visibility": 0.4, // 0..1, fraction of visibility through this tile
"resources": ["wood", "herbs", "exotic_animals"],
"climate": "tropical",
"elevation": 30, // metres
"biome": "jungle",
"encounters": "gas://encounter/jungle_predators@1.0.0"
}
2.2 Regional Map (square, medium scale)
A zoomed-in region. Tiles are sub-kilometre.
Jungle region:
- grass, mud, water, roots, dense_vegetation, trees, cliffs, caves, ruins, paths
Desert region:
- sand, dunes, rock, oasis, dry_river, canyon, ancient_ruins, nomad_camps
Ice region:
- snow, ice, frozen_lake, glacier, pine_forest, ice_cave, frozen_ruins
2.3 City Builder (square, small scale)
A different system entirely. Categories:
- Streets: dirt_road, cobblestone, stone_road, marble_road, bridge, alley
- Buildings: house, shop, inn, temple, castle, warehouse, farm, tower, gate, wall
- Districts: poor_quarter, market, noble_district, harbor, military, industrial, temple_district
2.4 Adventure / Dungeon (square / hex / freeform, tactical scale)
Grid-based combat maps. Two families ship by default:
Stone Dungeon:
- floor, wall, corner, door, secret_door, pillar, stairs, pit, water, lava
Cave:
- rock_floor, natural_wall, crystal, underground_lake, roots, mushrooms
Ancient Temple:
- marble_floor, columns, statues, altars, runes, trap_tiles
A pack can extend any family with new tile kinds (e.g. "ice dungeon" adds frozen floor and ice walls).
3. Tile package format
A tile package is a directory with a manifest (rules.json) and an asset tree:
jungle-adventure-pack/
├── manifest.json
├── terrain/
│ ├── grass.svg
│ ├── mud.svg
│ └── water.svg
├── tiles/
│ ├── jungle_floor.svg
│ └── jungle_path.svg
├── objects/
│ ├── palm_tree.svg
│ └── ruin.svg
├── decorations/
│ ├── flowers.svg
│ └── vines.svg
├── 3d/ # optional 3D equivalents
│ ├── palm_tree.glb
│ ├── ruin.glb
│ └── LOD/
│ ├── palm_tree_far.glb
│ └── ruin_far.glb
└── preview.png
manifest.json declares pack metadata, biome compatibility, variants, and rules.
3.1 Vector tile format (SVG)
SVG is the preferred primary format. Raster fallbacks (PNG/WEBP) are auto-derived.
{
"id": "ancient_tree",
"kind": "decoration", // 'terrain' | 'object' | 'decoration'
"variants": [
{ "id": "ancient", "path": "objects/ancient_tree.svg" },
{ "id": "young", "path": "objects/young_tree.svg" }
],
"tags": ["forest", "jungle", "large"],
"collision": false,
"gameplay": {
"blocksMovement": false,
"blocksVisibility": false,
"interactable": false
},
"rendering": {
"stackOrder": 5, // z-order hint
"anchor": "bottom-center",
"scale": [0.9, 1.3],
"rotationSnaps": [0, 90, 180, 270]
},
"3d": {
"mesh": "3d/palm_tree.glb",
"lod": ["3d/palm_tree.glb", "3d/LOD/palm_tree_far.glb"],
"heightMetres": 15,
"castsShadow": true
}
}
4. Procedural terrain generation
The user picks biome, density, style and age — and the generator produces the matching scatter (trees, ruins, paths, rivers) on top of the underlying biome map. Output is always editable vectors.
{
"generator": {
"biome": "jungle",
"density": 0.8,
"style": "ancient",
"age": 1000,
"climate": "rainy",
"fantasy": true
}
}
The generator runs:
- Place features — rivers, lakes, ruins, paths.
- Scatter objects — trees, rocks, flowers, decorations.
- Variants — each scatter picks a variant via seeded RNG.
- Output — appends
objects[]entries to the world document.
The user can then hand-edit any of the generated entries; subsequent regenerations respect the existing entries (preserve-on-regen).
4.1 Seeded determinism
Every generator run is seeded. The same seed + same biome map = same output. This is non-negotiable: worlds must round-trip identically.
5. Tile variants
Every tile ships N visual variants. The renderer selects one based on:
- Hash of tile coordinate — stable, deterministic.
- Theme override —
wintertheme selectsgrass_dead,grass_snowfirst. - Neighbour blending — wet/dry, lit/shaded variants.
Example — grass variants:
grass_01
grass_02
grass_flowers
grass_dark
grass_wet
grass_dead
grass_snow
Selection:
function pickVariant(tile: Tile, seed: number): string {
if (tile.theme === 'winter' && hasVariant('grass_snow', tile)) return 'grass_snow';
return variants[tile.id][seed % variants[tile.id].length];
}
6. Edge auto-tiling
A tile adjacent to a different biome gets a transition drawn between them. The renderer reads neighbour tiles and selects the matching edge variant.
Example — desert next to water:
water → wet_sand → sand → rocks
The asset pack ships every transition variant as a separate SVG. The renderer looks up the right one by inspecting all six (hex) or four (square) neighbours.
Implementation:
type EdgeKey = `${TileId}|${TileId}`;
const edgeRegistry = new Map<EdgeKey, string>([
['water|sand', 'transitions/water_to_sand.svg'],
['sand|rocks', 'transitions/sand_to_rocks.svg'],
// ...
]);
function edgeVariant(neighbours: Tile[]): string | null {
for (const n of neighbours) {
const key: EdgeKey = `${tile.id}|${n.id}`;
if (edgeRegistry.has(key)) return edgeRegistry.get(key)!;
}
return null;
}
7. Terrain transitions
Pack-level rules let the author declare allowed adjacency:
{
"transitions": {
"water": { "adjacent": ["beach", "swamp"] },
"forest": { "adjacent": ["grass", "mountain"] },
"desert": { "adjacent": ["oasis", "rocks"] }
}
}
The terrain-gen runtime uses these rules to:
- Validate biome maps at save time.
- Auto-place transition tiles when the user brushes across a boundary.
- Refuse to place illegal adjacencies (unless the user explicitly toggles "expert mode").
8. Theme packs
A theme pack is a re-skinning. It does not change terrain semantics — a jungle is still a jungle. It changes the visual + 3D asset set used to render it.
Medieval Fantasy → Viking → Samurai → Pirate Caribbean → Dark Souls → Sci-Fi
Same world. Different assets.
A theme pack is just a .gas-theme-pack with the same directory layout as a tile
pack, plus a theme.json:
{
"id": "viking",
"name": "Viking Northlands",
"biomeMap": {
"jungle": { "terrain": "dense_birch_forest", "objects": ["birch", "rune_stone"] },
"desert": { "terrain": "tundra", "objects": ["ice_boulder"] },
"ocean": { "terrain": "fjord" }
},
"palette": {
"primary": "#3a4a3a",
"secondary": "#6c5b3a",
"accent": "#a13d2f"
},
"ambient": { "color": "#8090a0", "intensity": 0.4 }
}
When the user applies a theme:
- The biome map resolves new
terrainandobjectsids per pack. - The 2D renderer swaps the SVG asset set.
- The 3D renderer swaps the GLB asset set.
- Lighting and palette update.
The world document itself is unchanged — themes are reversible.
9. Importing existing assets
The asset pipeline accepts:
| Format | Source | Adapters |
|---|---|---|
| SVG | any vector tool | importSvg() |
| PNG / WEBP | raster sources | importRaster() |
Tiled .tsx / .json | Tiled map editor | importTiled() |
| DungeonDraft assets | DungeonDraft | importDungeonDraft() |
| Wonderdraft assets | Wonderdraft (where licence permits) | importWonderdraft() |
| Unity sprites / prefabs | Unity | importUnity() |
.gas-pack | game-asset-platform | native |
Each adapter produces a normalised tile/object entry conforming to the schema in
§3.1. Round-trip is lossless for GAS-native assets.
10. GAS asset identity
Every tile, decoration, and theme pack is a GAS asset with:
interface GasAssetRef {
id: GasAssetId; // 'gas://object/palm_tree@1.2.0'
variant?: string; // 'ancient' | 'decayed' | ...
licence?: string;
}
This lets the marketplace resolve and licence assets independently of any specific
world. See 10-marketplace-and-packs.md.
11. Pack authoring
Creators build packs in the Pack Studio (a constrained WorldForge mode) and publish them to the GAS marketplace. The same pack can be bought once and used in unlimited worlds.
A pack author ships:
- Assets: SVG / PNG / GLB / rules.json.
- Manifest: pack metadata, themes, biome map.
- Preview: rendered hero shot.
- Licence: CC-BY / CC0 / commercial / etc.
See 10-marketplace-and-packs.md for the publishing
flow.
12. Performance
- Variants are loaded lazily per tile id and cached.
- 3D meshes use LOD (level of detail) —
LOD0for close,LOD2for far. - Draw-call batching — same-material tiles are drawn in a single call.
- Culling — off-screen tiles don't render in either 2D or 3D.
The asset pipeline never holds more than the active viewport's worth of decoded asset data in memory.