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

FieldTypeRequiredDescription
idstringYesUnique biome identifier, e.g. "mymod:cavern". Using the same id as a built-in biome overrides it.
namestringYesDisplay name shown in the HUD and level select.
orderintYesSort position used to sequence biomes outside the active campaign, and as a tiebreaker. The active campaign drives the play order. Lower values sort earlier.
levelsintYesTotal number of levels in this biome. The last level is the boss fight.
grid.base_widthintYesArena grid width on level 1.
grid.base_heightintYesArena grid height on level 1.
grid.width_growthintNoColumns added per level. Default 0.
grid.height_growthintNoRows added per level. Default 0.
floor_blocksstring[]YesBlock ids used for the arena floor, drawn in a checkerboard pattern.
obstacle_blocksstring[]NoBlock ids placed as obstacles. Default none.
obstacle_densityfloatNoBase probability (0.0 to 1.0) that a tile spawns an obstacle. Default 0.
obstacle_density_growthfloatNoAdditional obstacle probability added per level. Default 0.
environmentstringNoEnvironment 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.
nightboolNoWhen true the arena is treated as nighttime, preventing undead from burning. Default false.
enemiesobjectYesContainer for the enemy pools. Must be present even if all sub-fields use their defaults.
enemies.passivearrayNoWeighted pool of passive mobs that populate the arena alongside hostile enemies.
enemies.hostilearrayNoWeighted pool of hostile mobs that spawn each level.
enemies.bossobjectNoSingle enemy definition used for the boss level.
lootarrayYesWeighted item table. Craftics rolls 1 to 3 items per level from this pool.
enchantment_lootarrayNoOptional 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

FieldTypeRequiredDescription
idstringYesUnique environment id, e.g. "mymod:aether". Referenced by biomes with their "environment" field.
floor_blockstringNoBlock id used for normal arena floor tiles. Default minecraft:grass_block.
post_blockstringNoBlock id used for the arena border light-posts. Default minecraft:oak_fence.
light_blockstringNoBlock id placed on top of each light-post. Default minecraft:lantern.
decor_stylestringNoFlavor-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

MethodDefaultDescription
floorBlock(Block)Blocks.GRASS_BLOCKBlock used for normal arena floor tiles.
postBlock(Block)Blocks.OAK_FENCEBlock used for arena border light-posts.
lightBlock(Block)Blocks.LANTERNBlock placed atop each light-post.
decorStyle(String)This environment's own idFlavor-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

FieldTypeRequiredDescription
idstringYesUnique campaign id, e.g. "mymod:descent".
display_namestringNoCampaign name shown to players. Defaults to the id.
regionsarrayYesOrdered, non-empty list of region objects. Regions are played in order; the last node of the last region is the final boss.
branchobjectNoOptional branch definition (see below). Omit for a purely linear campaign.

Region fields

FieldTypeRequiredDescription
idstringYesRegion id, unique within the campaign. Referenced by branch.region.
display_namestringNoRegion name shown on its tab. Defaults to the id.
colorstringNoChat color code for the region label/tab, e.g. "§a". Default "§f" (white).
iconstringNoUnicode glyph shown on the region tab, e.g. "". Default "?".
map_colorstringNoARGB 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).
nodesarrayYesOrdered, non-empty list of node objects played in order within the region.

Node fields

FieldTypeRequiredDescription
biomestringYesBiome id this node plays. Should match a biome registered from a craftics/biomes/ datapack or via the API.
labelstringNoOptional 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.

FieldTypeRequiredDescription
regionstringYesThe id of the region this branch applies to.
swapstring[] or array[]YesTwo 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
));
FieldTypeDescription
idStringUnique event identifier, e.g. "mymod:treasure_chest".
displayNameStringName shown in the event popup.
probabilityfloatChance the event triggers after each level (0.0 to 1.0).
minBiomeOrdinalintThe earliest biome index (0 = first biome) where this event can appear. Use this to keep late-game events from appearing in early biomes.
isChoiceEventbooleanWhen true, the event presents the player with a set of options to choose from.
handlerEventHandlerThe 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

BlockCountPurpose
craftics:scene_spawnExactly oneWhere 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_markerTwo per boothCorner 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_markerOne per booth, inside the corner rectangleSits 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

  1. Build the booth structures and walkways however you like, using any blocks. This is the visible scene.
  2. Place one craftics:scene_spawn on the floor where the player should appear, facing the booths.
  3. At each booth, place two craftics:stand_marker blocks at opposite corners of the area you want to be clickable for that booth.
  4. Place one craftics:npc_marker inside 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.
  5. 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).
  6. Drop the file at data/craftics/scenes/village.schem or data/craftics/scenes/barter_station.schem in a datapack, or override it on disk by placing it in a craftics_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 idMeaning
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:addonOverflow 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:addonOverflow 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.