Campaigns
Build your own ordered playthrough of biomes that replaces the vanilla progression, drives difficulty scaling, and decides when New Game Plus begins.
What is a campaign
A campaign is an authored, ordered run through a set of biomes. You group those biomes into regions, and each region holds an ordered list of nodes. A node is a pointer to one biome plus an optional map label. The flattened run order across every region is the path your players walk, and that order drives difficulty scaling, the level select, dimension labeling, and completion detection.
Three rules shape everything below.
- Full replace. A registered campaign replaces the built-in path entirely. Campaigns do not merge. You author the whole run, start to finish.
- One active per world. The most recently registered non-vanilla campaign wins. If nothing else is registered, Craftics falls back to its built-in
craftics:vanillacampaign. - Last node of the last region is the final boss. Beating it triggers New Game Plus. This is purely positional. It does not matter how many regions or nodes you have.
This page is a how-to walkthrough. For the exhaustive field tables see the World page campaign schema.
Before you start
A campaign only references biomes by id. It never defines a biome itself. So before a campaign is playable you need the biomes it points at. Biomes are data driven and live at data/<namespace>/craftics/biomes/<biome_id>.json.
If a node references a biome that is not registered, the campaign still loads but that node is unreachable because there is nothing to play. The campaign loader does not check biome existence, so this is on you to get right. Register every biome a node names.
Here is the minimal biome JSON a node needs. Drop this at data/mymod/craftics/biomes/wildwood.json.
{
"id": "mymod:wildwood",
"name": "Wildwood",
"order": 10,
"levels": 5,
"grid": { "base_width": 8, "base_height": 8 },
"floor_blocks": ["minecraft:grass_block", "minecraft:moss_block"],
"enemies": {
"hostile": [
{ "type": "minecraft:zombie", "weight": 10, "hp": 12, "attack": 3 }
],
"boss": { "type": "minecraft:zombie", "weight": 1, "hp": 40, "attack": 5 }
},
"loot": [
{ "item": "minecraft:emerald", "weight": 5 }
]
}
The required fields are id, name, order (controls where the biome inserts into progression), levels, a grid with base_width and base_height, floor_blocks, an enemies object, and a loot array. Everything else is optional. For obstacles, environment themes, night, enchantment loot, and the enemy reference form see the full biome schema on the World page.
Make a campaign with a datapack
This is the route most authors should use. A campaign is one JSON file at data/<namespace>/craftics/campaigns/<id>.json. It is read on server start and re-read on /reload.
Step 1: a one region linear campaign
Start small. Put this at data/mymod/craftics/campaigns/descent.json. It has one region and two nodes, each pointing at a biome you also defined under craftics/biomes/.
{
"id": "mymod:descent",
"display_name": "The Descent",
"regions": [
{
"id": "surface",
"display_name": "Surface",
"color": "§a",
"icon": "",
"map_color": "FF6BBF59",
"nodes": [
{ "biome": "mymod:village", "label": "Quiet Village" },
{ "biome": "mymod:wildwood", "label": "The Wildwood" }
]
}
]
}
Field by field. id is required and must be non-blank. Use a namespaced id so it never collides with another addon. display_name is what players see and defaults to the id if you omit it. regions is a required non-empty array. Each region needs a non-blank id and a non-empty nodes array. Each node needs a non-blank biome. The label is optional and overrides the node's map label, falling back to the biome's own display name when absent.
The region presentation fields color, icon, and map_color are all optional. If you leave them out the region still works with sensible defaults. The next section covers what they drive.
Step 2: grow into multiple regions
Regions play in the order you list them. Add a second region and its nodes continue the run after the first region ends. The last node of the last region is the final boss.
{
"id": "mymod:descent",
"display_name": "The Descent",
"regions": [
{
"id": "surface",
"display_name": "Surface",
"color": "§a",
"icon": "",
"map_color": "FF6BBF59",
"nodes": [
{ "biome": "mymod:village", "label": "Quiet Village" },
{ "biome": "mymod:wildwood", "label": "The Wildwood" }
]
},
{
"id": "depths",
"display_name": "The Depths",
"color": "§b",
"icon": "",
"map_color": "FF4488CC",
"nodes": [
{ "biome": "mymod:frozen_cavern", "label": "Frozen Cavern" },
{ "biome": "mymod:abyss", "label": "The Abyss" }
]
}
]
}
Here the flattened run order is village, wildwood, frozen_cavern, abyss. The abyss node is the final boss because it is the last node of the last region. For every field and validation rule, including exactly what makes the loader skip a file, see the World page campaign schema.
Regions and the level select
Each region becomes a tab in the level select. The presentation fields control how that tab and its map tint look.
| Field | Default | Drives |
|---|---|---|
display_name | region id | The tab label. |
color | §f (white) | Minecraft chat color code used for the region accent. |
icon | ? | A single glyph shown on the tab. |
map_color | opaque white | The map tint for the region, as a hex string. |
map_color is a hex string with an optional leading #. Eight hex digits are read as ARGB, so "FF6BBF59" is opaque green. Six hex digits are read as RGB and get an opaque alpha added, so "6BBF59" is the same green. An absent value is opaque white with no warning. A malformed or wrong-length value falls back to opaque white and logs a warning, so check your logs if a region looks white when you did not intend it.
Art is optional. A biome card looks for art at textures/gui/biomes/<id>.png. If that file is missing the card falls back to a colored panel using your map_color and color. You can ship a fully working campaign with no custom art at all and add it later.
Branches
A branch is an optional within-region swap. It lets one region present two play orders. When the run uses branch choice 1 the two named biomes or segments swap places. When the run uses choice 0 the region plays in its listed order. Both choices play all the biomes. Only the order changes.
The single-biome swap names two biomes as two strings.
"branch": {
"region": "surface",
"swap": ["mymod:village", "mymod:wildwood"]
}
The segment swap names two contiguous runs of biomes as two arrays. Anything between the two segments stays put as a pivot, and the nodes around them stay in place.
"branch": {
"region": "surface",
"swap": [["mymod:desert", "mymod:jungle", "mymod:forest"], ["mymod:snowy", "mymod:mountain"]]
}
A branch is only honored when it is valid. The named region must exist, and both segments must be contiguous, disjoint, and non-interleaving sublists of that region's node order. An invalid but well-formed branch never throws. It quietly falls back to the linear order and logs a warning once. So if a swap is not taking effect, check the logs and confirm both segments sit next to each other in the region.
The built-in vanilla campaign is the real-world example. Its overworld region defines a segment swap between a warm trio (desert, jungle, forest) and a cool pair (snowy, mountain).
Completion and New Game Plus
The last node of the last region in the flattened run order is the final boss. Beating it triggers New Game Plus. This is count-agnostic. It works the same whether your campaign has one region with one node or five regions with dozens of nodes.
A one region, one node campaign is completely valid. That single node is both the start and the final boss, so clearing it rolls the run into NG+.
Develop and test
Campaign and biome JSON are both hot-reloadable. Run /reload in-game and your edits to craftics/campaigns/ and craftics/biomes/ are picked up without a restart. This is your core dev loop: edit JSON, /reload, open the level select, look.
To confirm your campaign is active, open the level select and check that it shows your regions and not the default Overworld, Nether, and End. If you still see the vanilla regions, your campaign was not registered or another campaign won the resolution.
The first biome in your run order is the unlock gate. It should always be startable. If your first node is locked, the campaign did not resolve as active or its first biome is not registered.
The world saves the active campaign as a stamp (activeCampaignId). Switching a world's active campaign mid-run is not supported and logs a warning. Pick the campaign for a world before you start playing it.
Common pitfalls
- Node biome not registered. The campaign loads but the node is unreachable. Make sure every node
biomehas a matchingcraftics/biomes/<id>.jsonor aregisterBiome(...)call. - Invalid map_color. Falls back to opaque white and logs a warning. Use 8-digit ARGB or 6-digit RGB hex.
- Invalid branch. Falls back to the linear order and logs a warning once. Confirm both segments are contiguous in the region.
- Forgetting full replace. Your campaign replaces vanilla entirely. There is no merging, so you must author the whole run including its final boss.
The Java / addon route
If you ship a code addon you can register a campaign in Java instead of JSON. The two routes are equivalent. A code-registered campaign even survives /reload where datapack campaigns are dropped and re-loaded. Most authors should still prefer JSON. Reach for Java only when you are already writing an addon and want to build the campaign programmatically.
An addon implements CrafticsAddon and is declared in fabric.mod.json under the "craftics" entrypoint key.
"entrypoints": {
"craftics": [
"com.example.mymod.MyCrafticsAddon"
]
}
Craftics calls onCrafticsInit() once, after all of its own built-in content is registered, so your registrations run last and override same-keyed built-ins. Build the campaign with the fluent builders and hand it to CrafticsAPI.registerCampaign(...).
Import note. The campaign classes live in com.crackedgames.craftics.level.campaign, not ...api.campaign. Only CrafticsAPI and CrafticsAddon live under ...api.
import com.crackedgames.craftics.api.CrafticsAddon;
import com.crackedgames.craftics.api.CrafticsAPI;
import com.crackedgames.craftics.level.campaign.Campaign;
import com.crackedgames.craftics.level.campaign.CampaignRegion;
import com.crackedgames.craftics.level.campaign.CampaignBranch;
import java.util.List;
public final class MyCrafticsAddon implements CrafticsAddon {
@Override
public void onCrafticsInit() {
CrafticsAPI.registerCampaign(
Campaign.builder("mymod:descent")
.displayName("The Descent")
.region(
CampaignRegion.builder("surface")
.displayName("Surface")
.color("§a")
.icon("")
.mapColor(0xFF6BBF59)
.node("mymod:village", "Quiet Village")
.node("mymod:wildwood", "The Wildwood")
.build()
)
.region(
CampaignRegion.builder("depths")
.displayName("The Depths")
.color("§b")
.mapColor(0xFF4488CC)
.node("mymod:frozen_cavern", "Frozen Cavern")
.node("mymod:abyss", "The Abyss")
.build()
)
// Single-biome swap convenience
.branch(CampaignBranch.of("surface", "mymod:village", "mymod:wildwood"))
.build()
);
}
}
Note the builder uses a singular region(...) method called once per region. There is no plural regions(...). For a multi-biome segment swap, build the branch with the canonical constructor instead of of(...).
// Segment swap: two contiguous lists
.branch(new CampaignBranch(
"surface",
List.of("mymod:desert", "mymod:jungle", "mymod:forest"),
List.of("mymod:snowy", "mymod:mountain")
))
If you need biomes in code too, register them with CrafticsAPI.registerBiome(...). Most addons define biomes as JSON and only use the Java route for the campaign itself. Pure JSON content needs no addon class at all because it is discovered automatically.
Worked example
Two reference implementations ship with the project. Copy whichever fits your scope.
- The example addon. Under
examples/addon-templateyou get a complete, minimal campaign atdata/exampleaddon/craftics/campaigns/example.jsonwith one region and one node, plus the biome it references atdata/exampleaddon/craftics/biomes/highlands.json. That single node is both the start and the final boss, so it is the smallest legal campaign you can ship. - The vanilla campaign. The built-in
craftics:vanillacampaign atdata/craftics/craftics/campaigns/vanilla.jsonis the full-scale reference. It has three regions (Overworld, Nether, End), per-region colors and icons, ARGB map colors with semi-transparent alpha, and a multi-biome segment branch in the overworld. Read it when you want to see every feature used together.
For the complete field tables and validation rules behind everything here, keep the World page campaign schema and the biome schema open while you build. The Reference page covers related data such as arena maps and environment themes.