← PETAFAB: Bot Invasion

Maps & Steam Workshop

Build your own battlefield.

Shape destructible terrain, fill it with fluid, and send in the bots. A PETAFAB map starts with a plain text JSON file. Edit it, play it, and share it through the in-game Workshop.

ZIP · 1.2 MB · Complete map JSON, editor schema, thumbnail and original capture

You’ll need PETAFAB on Steam for Windows or macOS and a text editor. Spillway uses built-in game assets, so there’s nothing to compile.

From a few brushes to a whole world

Two maps. One format.

Spillway gameplay: a steel wall with a doorway, a cylindrical fluid tank and hazard-striped bot gates.

Spillway

A small arena built from nine brushes, four built-in materials, a fluid tank and two enemy gates. The complete map is shown below and included in the source download.

Tumblebrook, a Claybook-inspired waterworks with colorful layered terrain, towers, bridges and blue water flowing down flumes.

Tumblebrook

The Claybook-inspired example takes the same format further: layered clay terrain, custom materials, a pressurized cistern, flowing water, lit caves and a final laser boss.

01 / Edit & play

Your first map

  1. Extract the example. Download Spillway into a new folder, such as MySpillway. Open that folder: level.json and preview.jpg should be directly inside it.
  2. Play it once. In PETAFAB, open Workshop → My Levels → Play Folder and select that folder. You can also open the download directly with Play Local ZIP.
  3. Make a small edit. Open level.json in a text editor. Try one of the changes below, save the file, then return to Workshop and choose Play Folder again. Each launch reads your edits from disk; restarting the current run uses the already-loaded map.
  4. Tune the look. While playing your local map, press F8 to adjust lighting, fog, bloom and sun settings. Choose Save JSON and look for SAVED LOCALLY. If you edited the file externally, reopen the map before saving from F8.

Try these changes

  • More bots: change both count values in the first wave from 3 to 5. Each gate will send five constructors.
  • Harmless fluid: set fluidDamagePerSecond to 0. The liquid still flows and pushes actors.
  • A different wall: change the wall’s builtin from steel to factory:steel.
  • Explore in peace: set waves to []. With no waves or boss, the map becomes a free-exploration space.

The included JSON Schema provides field hints and validation in compatible editors. Keep it next to level.json. Use valid JSON: double-quoted names, commas between entries, and no comments or trailing commas. Local testing earns no Workshop trophies or campaign progress.

02 / Build the arena

How a level works

Maps use version: 2. The required fields are version, materials, player, brushes, fluids, gates and waves. Empty arrays are valid. Field names are case-sensitive; unknown and duplicate fields are rejected.

Position, scale and direction

Positions are [x, y, z] in metres, with Y pointing up. Terrain covers X/Z from −32 to +32 and Y from 0 to 32. Every map begins with a destructible concrete slab whose top is at Y = 4. The player position is the lower capsule center, so a Y coordinate of 4.55 places the player on that slab. Leave headroom and put an ammo pickup nearby: the player starts unarmed.

Yaw is in degrees: 0 faces +Z and 90 faces +X. Simulation timing uses 120 ticks per second: 60 ticks is half a second.

Add shapes, then cut into them

The brushes array runs in order. An add brush creates solid terrain with a declared material; a subtract brush cuts terrain that already exists. Spillway first adds a wall, then cuts its doorway. It makes the tank in the same way: add a cylinder, then subtract a smaller cylinder to hollow it out.

ShapeDimensions
boxcenter and full size: [width, height, depth]. Optional yaw, pitch and corner rounding.
spherecenter and radius.
cylindercenter, radius and full height. Use topRadius to taper it; zero makes a cone.
toruscenter, centerline radius and tubeRadius. Optional pitch and yaw.

Materials, fluid and light

Each material has a map-local id, which brushes reference. Start with built-in names such as concrete, steel, teal and hazard. Prefix a name with yard:, factory: or rooftop: to choose that campaign palette. The game has 32 physical material slots shared by terrain, foam and bot remains; Spillway uses 11.

Fluid volumes seed the liquid; brushes form the vessel that contains it. A fluid cylinder uses bottom, radius and height. A pipe uses start, end and radius. Keep the whole fluid volume inside the terrain bounds. Breach the tank and the liquid spills.

sun sets directional lighting, lights adds point lights, and render controls the image’s appearance. You can adjust these in the file or use F8 while playing your own map.

Gates and enemy waves

A gate is a spawn volume, separate from its visible terrain frame. Give it a unique id, a floor position at the bottom center of its rear face, full size, and a yaw pointing into the arena. Build a clear floor and exit; keep the entire volume inside X/Z ±31.75 and Y 0…32. Maps support up to four gates and 32 waves.

Each wave contains one to four groups. A group names its gate, enemy, count, and either intervalTicks or intervalSeconds. Enemy types are constructor, runner, rocketeer and shock_trooper.

A new wave waits for earlier groups to finish spawning and for the cumulative afterDefeats threshold. If omitted, that threshold is all enemies in earlier waves. afterSeconds adds a minimum time since the previous wave’s last spawn; delayTicks adds a further delay once the conditions are met.

In Spillway, each gate sends three constructors at half-second intervals. Once all six have arrived and four have been defeated, a 180-tick delay passes, then two runners enter from the west. Clear all eight enemies to win.

For a final encounter, add a top-level boss with a type of barrage, saw or laser and a body-center position. Defeating the boss wins the map. Set announcement: "none" on an individual wave or boss to silence its arrival banner, alarm and voice; boss music still plays.

03 / The complete example

Spillway, in one file.

Download level.json ↓

This is the complete, editable level.json from the ZIP. Geometry is defined by the brushes; materials, sky and bots are supplied by PETAFAB. No separate models or textures are needed for this example.

{
  "$schema": "./level.schema.json",
  "version": 2,
  "fluidDamagePerSecond": 40,
  "materials": [
    {"id": "wall", "builtin": "steel"},
    {"id": "tank", "builtin": "teal"},
    {"id": "gate", "builtin": "hazard"},
    {"id": "plinth", "builtin": "concrete"}
  ],
  "botMaterials": {"constructor": {"robot_joint": "wall"}, "runner": {"robot_joint": "wall"}},
  "player": {"position": [0, 4.55, -11], "yaw": 0},
  "sun": {"direction": [0.4, 0.72, 0.6], "radiance": [5.5, 4.7, 3.9], "ambient": 2},
  "render": {"exposure": 1.15, "contrast": 0.05, "saturation": 0.12, "bloom_strength": 0.65, "fog_density": 0.003},
  "lights": [
    {"position": [-11, 6.5, 0], "radius": 8, "radiance": [20, 4, 0.5]},
    {"position": [11, 6.5, 9], "radius": 8, "radiance": [1, 6, 20]},
    {"position": [8, 7.4, 0], "radius": 5, "radiance": [0.6, 9, 6]}
  ],
  "pickups": [
    {"kind": "ammo", "position": [0, 4.55, -10]},
    {"kind": "grenade", "position": [2, 4.55, -8]}
  ],
  "brushes": [
    {"op": "add", "shape": "box", "center": [0, 6, 3], "size": [13, 4, 1.5], "material": "wall"},
    {"op": "subtract", "shape": "box", "center": [0, 5.5, 3], "size": [3, 3.5, 3]},
    {"op": "add", "shape": "cylinder", "center": [8, 5.5, 0], "radius": 2.8, "height": 3, "material": "tank"},
    {"op": "subtract", "shape": "cylinder", "center": [8, 6.1, 0], "radius": 2.3, "height": 3.2},
    {"op": "add", "shape": "box", "center": [-12, 6.5, 0], "size": [1, 5, 8], "material": "gate"},
    {"op": "subtract", "shape": "box", "center": [-12, 5.8, 0], "size": [2, 4, 5]},
    {"op": "add", "shape": "box", "center": [12, 6.5, 9], "size": [1, 5, 8], "material": "gate"},
    {"op": "subtract", "shape": "box", "center": [12, 5.8, 9], "size": [2, 4, 5]},
    {"op": "add", "shape": "box", "center": [-5, 4.8, -4], "size": [3, 1.6, 1.4], "yaw": 25, "material": "plinth"}
  ],
  "fluids": [
    {"shape": "cylinder", "bottom": [8, 4.6, 0], "radius": 2, "height": 2}
  ],
  "gates": [
    {"id": "west", "floor": [-12, 4, 0], "size": [4, 4, 4], "yaw": 90},
    {"id": "east", "floor": [12, 4, 9], "size": [4, 4, 4], "yaw": -90}
  ],
  "waves": [
    {"delayTicks": 120, "groups": [
      {"gate": "west", "enemy": "constructor", "count": 3, "intervalTicks": 60},
      {"gate": "east", "enemy": "constructor", "count": 3, "intervalTicks": 60}
    ]},
    {"afterDefeats": 4, "delayTicks": 180, "groups": [
      {"gate": "west", "enemy": "runner", "count": 2, "intervalTicks": 90}
    ]}
  ]
}

04 / Go further

Custom materials & skies

Once your layout plays well, materials can reference your own cooked DDS textures instead of built-in names. Keep those files inside the map folder and use relative paths with forward slashes, such as textures/stone_color.dds.

  • Surface textures: pair a 1024 × 1024 BC7 sRGB color DDS with a BC5 linear tangent-space normal DDS. Both need 11 mip levels and tile once per metre. Material declarations use color and normal paths, with optional roughness, metallic and emission.
  • Fluid: a top-level fluidMaterial can supply its own color/normal pair and surface settings. fluidDamagePerSecond controls damage independently.
  • Sky: skybox takes three unsigned BC6H cubemaps: sky (1024² per face, 11 mips), diffuse (16², one mip), and specular (256², seven mips).

Put editable images and author notes under source/; keep all runtime DDS files outside that folder. Tumblebrook uses custom clay and water materials, layered brush geometry and room lighting to build its waterworks. The schema reference documents the available fields and ranges.

05 / Share your map

Publish to Steam Workshop

  1. Test your saved map. Open it with Play Folder or Play Local ZIP. Check the spawn, ammo, paths out of both gates, fluid behavior and victory condition.
  2. Add a thumbnail. Save a JPEG named preview.jpg, smaller than 1 MiB, beside level.json. Use a picture of your edited map. If you make a ZIP manually, put both files at its root, not inside another folder, and preserve referenced DDS paths.
  3. Create the listing. Choose Workshop → My Levels → New Level and select your folder or ZIP. The last local map you played is preselected. Enter a title, description and any tags or change note.
  4. Choose what to share. Leave Skip source/ folder checked for a playable upload. Uncheck it to include your editable assets and notes. Folder uploads gather the JSON, referenced DDS files and thumbnail automatically.
  5. Upload and make it public. Open Read Steam Terms, accept the terms for this submission, then choose Upload New Item. New items start Private. Finish any agreement acceptance on the Steam item page, test the published copy, and set its visibility to Public when ready. Keep Steam running until the upload finishes.

Update the same item

Open your existing map’s Community → Update, choose the revised folder or ZIP, and select Update Level. This keeps the item and its subscribers together. For a listing-only edit, clear the selected folder or ZIP to keep the existing content and thumbnail.

Package limits & common fixes
  • Folder not recognized: select the folder containing level.json, not its parent. For a ZIP, keep level.json and preview.jpg at the archive root.
  • JSON rejected: check editor errors, spelling, required fields and references to material or gate IDs. The game also validates geometry and resource budgets.
  • Edits not showing: save in your editor, return to Workshop, and reopen the folder. Restarting the current run does not reload external edits.
  • Missing textures: include every referenced DDS at the exact relative path. Required runtime files must be outside source/.
  • Upload limits: 128 MiB for both the ZIP and expanded contents, at most 128 entries, 32 MiB of JSON, and 69 DDS files. Use ordinary stored or deflated ZIP entries with printable ASCII relative names; avoid encrypted files, links and duplicate names.

Need help? Visit PETAFAB support →