World
How to add custom biomes, arena environment themes, campaigns, between-level events, and Village / Bartering Station scenes to Craftics.
Biomes
Biomes are the arenas players fight through. Each biome has a grid size, floor and obstacle blocks, an environment theme, an enemy pool, and a loot table. The active campaign determines the order biomes are played in. A biome's order value sequences biomes that are not placed by the active campaign and acts as a tiebreaker, so an addon biome with "order": 50 sorts after any built-in biome with a lower order and before any with a higher one.
Biomes can be added two ways: through a JSON datapack file (no code required) or programmatically through CrafticsAPI.registerBiome(). The JSON path is simpler for pure content; the code path is needed when you want to share a BiomeTemplate object built entirely in Java.
JSON Datapack
Place a JSON file at data/<namespace>/craftics/biomes/<biome_id>.json. Craftics discovers every file at that path automatically on resource reload. Use /reload to hot-reload biome JSON without restarting the server.
{
"id": "mymod:cavern",
"name": "Cavern",
"order": 20,
"levels": 5,
"grid": {
"base_width": 10,
"base_height": 10,
"width_growth": 1,
"height_growth": 1
},
"floor_blocks": ["minecraft:stone_bricks", "minecraft:mossy_stone_bricks"],
"obstacle_blocks": ["minecraft:cobblestone_wall"],
"obstacle_density": 0.1,
"obstacle_density_growth": 0.02,
"environment": "cave",
"night": true,
"enemies": {
"passive": [
{"type": "minecraft:bat", "weight": 3, "hp": 2, "attack": 0, "defense": 0, "range": 1}
],
"hostile": [
{"type": "minecraft:skeleton", "weight": 8, "hp": 10, "attack": 3, "defense": 0, "range": 3},
{"type": "minecraft:spider", "weight": 6, "hp": 8, "attack": 3, "defense": 0, "range": 1},
{"enemy": "mymod:cave_witch", "weight": 3}
],
"boss": {"type": "minecraft:warden", "hp": 50, "attack": 8, "defense": 4, "range": 2}
},
"loot": [
{"item": "minecraft:diamond", "weight": 5},
{"item": "minecraft:iron_ingot","weight": 15}
],
"enchantment_loot": [
{"enchantment": "minecraft:sharpness", "weight": 3}
]
}
Field Reference
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique biome identifier, e.g. "mymod:cavern". Using the same id as a built-in biome overrides it. |
name | string | Yes | Display name shown in the HUD and level select. |
order | int | Yes | Sort position used to sequence biomes outside the active campaign, and as a tiebreaker. The active campaign drives the play order. Lower values sort earlier. |
levels | int | Yes | Total number of levels in this biome. The last level is the boss fight. |
grid.base_width | int | Yes | Arena grid width on level 1. |
grid.base_height | int | Yes | Arena grid height on level 1. |
grid.width_growth | int | No | Columns added per level. Default 0. |
grid.height_growth | int | No | Rows added per level. Default 0. |
floor_blocks | string[] | Yes | Block ids used for the arena floor, drawn in a checkerboard pattern. |
obstacle_blocks | string[] | No | Block ids placed as obstacles. Default none. |
obstacle_density | float | No | Base probability (0.0 to 1.0) that a tile spawns an obstacle. Default 0. |
obstacle_density_growth | float | No | Additional obstacle probability added per level. Default 0. |
environment | string | No | Environment id for the arena's visual theme. Built-in values: plains, forest, desert, jungle, river, mountain, snowy, cave, nether_wastes, end, deep_dark. Addon environments are accepted here too. Default plains. |
night | bool | No | When true the arena is treated as nighttime, preventing undead from burning. Default false. |
enemies | object | Yes | Container for the enemy pools. Must be present even if all sub-fields use their defaults. |
enemies.passive | array | No | Weighted pool of passive mobs that populate the arena alongside hostile enemies. |
enemies.hostile | array | No | Weighted pool of hostile mobs that spawn each level. |
enemies.boss | object | No | Single enemy definition used for the boss level. |
loot | array | Yes | Weighted item table. Craftics rolls 1 to 3 items per level from this pool. |
enchantment_loot | array | No | Optional list of enchantment ids with weights. When present, enchantment book drops are restricted to these entries. |
Enemy entries. Each object in passive, hostile, and boss accepts two forms. The inline form uses "type" (a Minecraft entity id) plus stat fields: hp, attack, defense, range, and speed (defaults: 6 / 2 / 0 / 1 / 0). The reference form uses "enemy": "<id>" to point at a registered enemy template, with an optional "weight"; the template supplies all stats and AI. The attack field is the base attack value. Craftics scales it up with biome depth, so keep base values modest (roughly 1 to 10).
Java API
To register a biome from code, build a BiomeTemplate and pass it to CrafticsAPI.registerBiome(). You can also call CrafticsAPI.getTotalLevels() at any time to query the total level count across all registered biomes.
// In your CrafticsAddon.onCrafticsInit():
CrafticsAPI.registerBiome(myBiomeTemplate);
// Query total level count
int totalLevels = CrafticsAPI.getTotalLevels();
Environments
An environment defines the visual theme of an arena: which block covers the floor, which block forms the border light-posts, which block sits on top of each post as a light source, and which built-in decoration style is used for flavor obstacles. Biomes select an environment with their "environment" field.
Craftics ships built-in environments for plains, forest, desert, jungle, river, mountain, snowy, cave, nether_wastes, end, and deep_dark. Addons can register new ones via a JSON datapack or the Java API.
JSON Datapack
Place a JSON file at data/<namespace>/craftics/environments/<env_id>.json. Only id is required; the block fields fall back to plains defaults (grass block floor, oak fence posts, lantern lights) when omitted.
{
"id": "mymod:aether",
"floor_block": "minecraft:quartz_block",
"post_block": "minecraft:quartz_pillar",
"light_block": "minecraft:sea_lantern",
"decor_style": "forest"
}
Field Reference
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique environment id, e.g. "mymod:aether". Referenced by biomes with their "environment" field. |
floor_block | string | No | Block id used for normal arena floor tiles. Default minecraft:grass_block. |
post_block | string | No | Block id used for the arena border light-posts. Default minecraft:oak_fence. |
light_block | string | No | Block id placed on top of each light-post. Default minecraft:lantern. |
decor_style | string | No | Flavor-obstacle style. Pass a built-in environment id (e.g. "forest") to reuse its obstacles. Defaults to this environment's own id (no flavor obstacles). |
Java API
Use EnvironmentDef.builder(id) for a fluent build, then pass the result to CrafticsAPI.registerEnvironment(). You can verify registration with CrafticsAPI.hasEnvironmentStyle(id).
import com.crackedgames.craftics.api.EnvironmentDef;
import net.minecraft.block.Blocks;
// In your CrafticsAddon.onCrafticsInit():
CrafticsAPI.registerEnvironment(
EnvironmentDef.builder("mymod:aether")
.floorBlock(Blocks.QUARTZ_BLOCK)
.postBlock(Blocks.QUARTZ_PILLAR)
.lightBlock(Blocks.SEA_LANTERN)
.decorStyle("forest")
.build()
);
// Check registration
boolean exists = CrafticsAPI.hasEnvironmentStyle("mymod:aether");
EnvironmentDef builder methods
| Method | Default | Description |
|---|---|---|
floorBlock(Block) | Blocks.GRASS_BLOCK | Block used for normal arena floor tiles. |
postBlock(Block) | Blocks.OAK_FENCE | Block used for arena border light-posts. |
lightBlock(Block) | Blocks.LANTERN | Block placed atop each light-post. |
decorStyle(String) | This environment's own id | Flavor-obstacle style. Pass a built-in environment id to reuse its obstacle decorations. |
Campaigns
A campaign is a full custom playthrough: an ordered list of regions, each holding an ordered list of biome nodes. The campaign is what actually drives the run, deciding which biomes the player fights through and in what order. Craftics ships a built-in craftics:vanilla campaign with three regions (Overworld, Nether, End).
Campaigns use full-replace semantics. A registered campaign replaces the vanilla progression entirely, rather than appending to it. Only one campaign is active per world: the most-recently-registered non-vanilla campaign wins, and vanilla is the default when no addon campaign is registered. This makes a campaign the right tool for a total-conversion playthrough, not for adding a few biomes alongside the base game.
The last node of the last region is the final boss. Beating it completes the campaign and triggers New Game+. This is count-agnostic, so a single-region campaign with one biome completes on that biome's boss, and a five-biome campaign completes on the fifth biome's boss.
JSON Datapack
Place a JSON file at data/<namespace>/craftics/campaigns/<id>.json. Craftics discovers every file at that path on resource reload. The canonical worked example is the built-in vanilla campaign at data/craftics/craftics/campaigns/vanilla.json.
{
"id": "mymod:descent",
"display_name": "The Descent",
"regions": [
{
"id": "surface",
"display_name": "Surface",
"color": "§a",
"icon": "",
"map_color": "FF6BBF59",
"nodes": [
{ "biome": "mymod:village" },
{ "biome": "mymod:wildwood", "label": "The Wildwood" }
]
}
],
"branch": {
"region": "surface",
"swap": ["mymod:village", "mymod:wildwood"]
}
}
Top-level fields
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique campaign id, e.g. "mymod:descent". |
display_name | string | No | Campaign name shown to players. Defaults to the id. |
regions | array | Yes | Ordered, non-empty list of region objects. Regions are played in order; the last node of the last region is the final boss. |
branch | object | No | Optional branch definition (see below). Omit for a purely linear campaign. |
Region fields
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Region id, unique within the campaign. Referenced by branch.region. |
display_name | string | No | Region name shown on its tab. Defaults to the id. |
color | string | No | Chat color code for the region label/tab, e.g. "§a". Default "§f" (white). |
icon | string | No | Unicode glyph shown on the region tab, e.g. "". Default "?". |
map_color | string | No | ARGB hex string for the region tint, e.g. "FF6BBF59". An 8-digit value is AARRGGBB; a 6-digit value gets opaque alpha. Malformed values default to white (FFFFFFFF). |
nodes | array | Yes | Ordered, non-empty list of node objects played in order within the region. |
Node fields
| Field | Type | Required | Description |
|---|---|---|---|
biome | string | Yes | Biome id this node plays. Should match a biome registered from a craftics/biomes/ datapack or via the API. |
label | string | No | Optional override for the node's map label. Defaults to the biome's own display name. |
Branch fields
The optional branch lets a single region present a choice. When the player picks branch choice 1, the two named biomes (or two named segments) are swapped. The two biomes or segments must be contiguous in the named region; an invalid swap falls back to a linear region and is logged.
| Field | Type | Required | Description |
|---|---|---|---|
region | string | Yes | The id of the region this branch applies to. |
swap | string[] or array[] | Yes | Two contiguous biomes to swap, as two strings (["a", "b"]), or two contiguous segments, as two arrays of strings ([["a", "b", "c"], ["d", "e"]]). Branch choice 1 plays them in swapped order. |
Java API
Build a campaign with Campaign.builder(id) and its CampaignRegion.builder(id) regions, then pass the result to CrafticsAPI.registerCampaign(). Use CampaignBranch.of(region, a, b) for the single-biome swap convenience, or construct a CampaignBranch directly with two lists for a segment swap.
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;
// In your CrafticsAddon.onCrafticsInit():
CrafticsAPI.registerCampaign(
Campaign.builder("mymod:descent")
.displayName("The Descent")
.region(
CampaignRegion.builder("surface")
.displayName("Surface")
.color("§a")
.icon("")
.mapColor(0xFF6BBF59)
.node("mymod:village")
.node("mymod:wildwood", "The Wildwood")
.build()
)
// Single-biome swap convenience
.branch(CampaignBranch.of("surface", "mymod:village", "mymod:wildwood"))
// Segment swap: new CampaignBranch("surface", List.of("a", "b"), List.of("c", "d"))
.build()
);
Events
Between-level events fire after a combat level completes. Craftics rolls through all registered events by probability and triggers one when the roll succeeds. Events are code-only for now: there is no JSON datapack loader for events. Register them through CrafticsAPI.registerEvent().
EventEntry fields
Construct an EventEntry record directly and pass it to CrafticsAPI.registerEvent().
import com.crackedgames.craftics.api.registry.EventEntry;
import com.crackedgames.craftics.api.EventHandler;
CrafticsAPI.registerEvent(new EventEntry(
"mymod:treasure_chest", // unique event id
"Treasure Chest", // display name shown in the event popup
0.15f, // probability per level (0.0 to 1.0)
2, // minBiomeOrdinal: earliest biome (0-indexed) where this can appear
true, // isChoiceEvent: true if the player picks from multiple options
myEventHandler // EventHandler implementation
));
| Field | Type | Description |
|---|---|---|
id | String | Unique event identifier, e.g. "mymod:treasure_chest". |
displayName | String | Name shown in the event popup. |
probability | float | Chance the event triggers after each level (0.0 to 1.0). |
minBiomeOrdinal | int | The earliest biome index (0 = first biome) where this event can appear. Use this to keep late-game events from appearing in early biomes. |
isChoiceEvent | boolean | When true, the event presents the player with a set of options to choose from. |
handler | EventHandler | The logic executed when the event fires. |
EventHandler interface
EventHandler is a functional interface. Its execute method receives the list of participating players, the server world, and the EventManager, which provides helpers for showing UI popups, giving items, applying effects, and managing event state.
@FunctionalInterface
public interface EventHandler {
void execute(List<ServerPlayerEntity> participants,
ServerWorld world,
EventManager eventManager);
}
Forcing an event
During development or for scripted sequences you can force a specific event to trigger at the next between-level transition for a given player. Pass null to clear any forced event.
// Force the next event for a specific player
CrafticsAPI.forceNextEvent(player, "mymod:treasure_chest");
// Clear the forced event
CrafticsAPI.forceNextEvent(player, null);
Example: custom event
CrafticsAPI.registerEvent(new EventEntry(
"mymod:healing_spring",
"Healing Spring",
0.10f,
1, // available from biome index 1 onwards
false, // not a choice event, it just fires
(participants, world, eventManager) -> {
for (ServerPlayerEntity player : participants) {
player.heal(4.0f);
}
}
));
EventTemplates. A future EventTemplates class will provide pre-built patterns for common event types such as gamble, reward, ambush, and trader. For now implement EventHandler directly as shown above.
Village & Bartering Station scenes
The Village and Bartering Station are walk-around hub scenes the player teleports into from the level select. A scene is built from a schematic, like an arena, using three scene marker blocks: one that sets where the player appears, a pair that marks the clickable area of each merchant booth ("stand"), and one that places the booth's NPC. You build the layout in-world or in a schematic editor, drop the .schem in the scenes folder, and Craftics scans the markers when the scene loads.
Marker blocks are invisible in the finished scene. craftics:scene_spawn, craftics:stand_marker, and craftics:npc_marker are placement markers, not decoration. When the scene is scanned, each marker is replaced with the most common block touching it, so it blends into the surrounding floor and the player never sees a marker block (the same way arena corner markers disappear). Give yourself the blocks in creative with /give @s craftics:scene_spawn, /give @s craftics:stand_marker, and /give @s craftics:npc_marker.
The marker blocks
| Block | Count | Purpose |
|---|---|---|
craftics:scene_spawn | Exactly one | Where the player materializes when they enter the scene, and the direction the third-person camera faces. Place it looking toward the booths: the block stores the way you were facing when you placed it, and the camera aims along that facing. If a scene has no spawn marker the scanner reports it as an authoring error and the scene will not open. |
craftics:stand_marker | Two per booth | Corner markers, placed in pairs. The two markers mark the opposite corners of a rectangle, and clicking anywhere inside that rectangle counts as clicking the booth. Corner markers hold no data of their own; the booth is identified by the NPC marker inside the rectangle. |
craftics:npc_marker | One per booth, inside the corner rectangle | Sits inside the booth's two-corner rectangle and is the booth's identity: it places the NPC (its position and facing) and carries the occupant id. The player walks up one tile in front of the NPC, facing it. Every booth needs exactly one NPC marker inside its corners; that is also how the scanner knows which two corners form which booth. |
Building a scene, step by step
- Build the booth structures and walkways however you like, using any blocks. This is the visible scene.
- Place one
craftics:scene_spawnon the floor where the player should appear, facing the booths. - At each booth, place two
craftics:stand_markerblocks at opposite corners of the area you want to be clickable for that booth. - Place one
craftics:npc_markerinside that rectangle, where the NPC should stand, facing the player's approach. This both positions the NPC and ties the two corners together into one booth. - Select the whole build (including the markers) and export it as a Sponge
.schem(WorldEdit / Litematica / FAWE all produce this format, the same one arenas use). - Drop the file at
data/craftics/scenes/village.schemordata/craftics/scenes/barter_station.schemin a datapack, or override it on disk by placing it in acraftics_scenes/folder next to the server run directory. The disk copy wins if both exist, so you can iterate without rebuilding the jar.
How booths are paired. For each NPC marker, the scanner finds the tight pair of stand corners whose rectangle contains it (and no other corner). Those two corners plus the NPC marker form one booth, and the rectangle is its clickable area. If an NPC marker has no clean containing pair around it (or the layout is ambiguous), that booth is skipped with a warning in the log and the rest of the scene still loads.
The village is for villager merchants; the bartering station is for piglins. The two scenes are authored identically. The only difference is which occupant ids you give their NPC markers (villager professions vs. piglin barter categories) and which schematic file name you save under.
Assigning booth occupants
A booth's occupant decides which merchant stands there, and it lives on the booth's npc_marker. Occupants are not baked into the schematic: schematic export saves block states but not a marker's stored data, so an NPC marker loaded from a .schem comes in blank. Instead the scene assigns occupants when it is built, keyed by booth index (the deterministic order the scanner produces, sorted by the rectangle's corner). This keeps "where the booths are" (the schematic) separate from "who staffs them" (live progression), so a booth can show its merchant for one player and sit empty for another who has not met them yet.
An occupant id is one of:
| Occupant id | Meaning |
|---|---|
craftics:weaponsmith (a villager profession id) or craftics:warmonger (a piglin barter category id) | Dedicated booth. Hosts exactly that one merchant. The booth is empty until the player has met that merchant during a normal level. |
villager:addon | Overflow booth. Hosts every met villager merchant that does not have a dedicated booth of its own (i.e. addon/modded traders). Walking up shows a choice of which one to talk to. |
piglin:addon | Overflow booth for met piglin barter categories that do not have a dedicated booth. Use this on the bartering station scene. |
Testing a booth in-world. While iterating you can set an NPC marker's occupant directly with a command, then re-scan the live world (rather than re-exporting):
/data merge block ~ ~ ~ {Occupant:"craftics:weaponsmith"}
Target the npc_marker and confirm it stuck with /data get block ~ ~ ~ Occupant. This is for ad-hoc testing only; the real scene populates occupants by booth index when it builds.
Reusing a scene as an addon
Because the scenes are loaded from data/<namespace>/craftics/scenes/<name>.schem via datapack, an addon can ship its own village.schem / barter_station.schem to replace the built-in layout, and the booth occupants resolve from the merchant registries at build time. A modded trader or barter category that has been met automatically appears at the matching overflow booth (villager:addon / piglin:addon) with no schematic change required.