Skip to content

Configuration

config/battletower-server.toml: which backends handle Battle Factory. Restart to apply.

  • server.eventServer (default events): backend for modes that use temporary Pokemon.
  • server.rewardAuthorityServer (default main): the only backend allowed to pay Factory rewards.

config/battletower/towers/<id>.json: one file per tower. Apply with /bt admin reload.

Tower file overview

{
  "id": "example_tower",
  "displayName": "Example Tower",
  "enabled": false,
  "rulesetId": null,
  "entry": {
    "command": "bt enter example_tower",
    "permission": "battletower.enter.example_tower",
    "limit": { "count": 3, "window": "PER_DAY" },
    "cost": null
  },
  "between_battles": { "healPlayerTeam": true, "delayTicks": 20, "denyMenus": true },
  "stages": [
    {
      "arenaId": "tower_arena_1",
      "trainer": { "displayName": "Trainer Mira", "aiMode": null, "canMega": false,
                   "canDynamax": false, "bossTier": null, "team": [ ... ] },
      "onWin": { "rewards": [ { "type": "MONEY", "amount": 200 } ] },
      "onLoss": { "endRun": true }
    }
  ],
  "onComplete": { "rewards": [ { "type": "ITEM", "amount": 5, "item": "pixelmon:rare_candy" } ] }
}

Picking trainers

Trainers are picked once, when the run starts, and stay fixed for that run.

Mode How stages get trainers Rules
(no trainerPool) Each stage has a fixed trainer One non-null trainer per stage
shared_random Distinct trainers drawn from one shared trainers list Shared list size >= stage count; stages have trainer: null
shuffle_all_trainers Shared list shuffled, one per stage Shared list size == stage count
per_stage_random Each stage picks from its own trainerPool list Every stage needs a non-empty pool
pick_random (legacy) Draw count whole stage definitions from trainerPool.stages count > 0 and <= pool size; top-level stages unused
shuffle_all (legacy) All trainerPool.stages in random order Top-level stages unused

Example, shared_random:

{
  "stages": [
    { "arenaId": "tower_1", "trainer": null, "onWin": { "rewards": [] }, "onLoss": { "endRun": true } },
    { "arenaId": "tower_2", "trainer": null, "onWin": { "rewards": [] }, "onLoss": { "endRun": true } }
  ],
  "trainerPool": {
    "mode": "shared_random",
    "trainers": [
      { "displayName": "Trainer A", "team": [] },
      { "displayName": "Trainer B", "team": [] },
      { "displayName": "Trainer C", "team": [] }
    ]
  }
}

Example, per_stage_random: set "trainerPool": { "mode": "per_stage_random" } at the top, and give each stage "trainer": null plus its own "trainerPool": [ {...}, {...} ].

The tower is rejected at load if: the mode is unknown or blank, legacy pool stages are mixed with top-level static stages, a rule in the table above is broken, or an arenaId is not a registered arena. The editor GUI keeps pool fields intact but cannot edit them; write pools in JSON.

Between battles

Key Meaning
healPlayerTeam Heal the party after a stage win
delayTicks Ticks before the next battle (default 20 if the block is missing; minimum 1 in practice)
denyMenus Block menus between battles (not implemented yet)

Rewards

Type Gives amount means
MONEY Vault money (through Battle Core) Currency
ITEM An item (item field) Item count
XP Experience levels Levels
COMMAND Runs a console command (command field) Ignored
  • MONEY is the only allowed entry cost type. Old Pixelmon-bank costs and rewards are converted to MONEY 1:1 on load, and the file is rewritten.
  • Where rewards go: stages[].onWin (first clear this period), stages[].onWinRepeat (later clears), onComplete and onCompleteRepeat (whole tower).

Reward reset periods (resetWindow)

Calendar periods in Eastern time. Everyone resets at the same moment.

resetWindow Resets at
PER_HOUR Top of the next hour
PER_DAY Next 12AM ET
PER_WEEK Next Monday 12AM ET
CUSTOM_DURATION, unset, unknown Never tracked: every run pays first-run rewards

Attempt limits work differently. entry.limit.window is a rolling lookback in Battle Core, so PER_DAY there means "in the last 24 hours". With a limit of N, an attempt frees up at the Nth most recent attempt plus the window length. The names match; the behavior does not.

A late clear buys a short period: a first clear at 11:50 PM ET resets ten minutes later. Times say "12AM ET" because the boundary follows daylight saving time.

Battle Factory

An optional factory block turns a tower into the rental format. The player is handed off to the events backend, picks 3 rentals out of 6 random offers, and may swap one rental for one used by each defeated non-final trainer. Battles use detached Pokemon, so the real party is never replaced or healed.

"factory": {
  "enabled": true,
  "choiceCount": 6,
  "teamSize": 3,
  "allowSwaps": true,
  "rentalPool": [
    { "species": "pikachu", "level": 50, "moves": ["thunderbolt", "nuzzle", "protect"] }
  ]
}
  • The pool needs at least choiceCount entries. Use 7 stages and 3 Pokemon per trainer for the classic format.
  • A Factory tower must be free to enter (no cost).
  • Source and events backends must share Battle Core's MariaDB and have the same Battle Tower, Battle Core, tower, ruleset, and arena configs. Rewards are paid on rewardAuthorityServer.

Trainer team fields

Wrong formats do not fail at load. Pixelmon logs a warning at battle time and drops the value.

Field Format Example
moves Title Case with spaces "Shadow Ball"
ability lowercase, no spaces "magicguard"
heldItem full item id "pixelmon:life_orb"
species name, any case, namespace optional "Alakazam"
form lowercase form name "alolan", "megay"
nature lowercase "timid"
gender MALE, FEMALE, GENDERLESS "FEMALE"
ivs / evs 6 numbers: HP, Atk, Def, SpA, SpD, Spe [31, 0, 31, 31, 31, 31]
  • aiMode: pixelmon:random, pixelmon:no_op, pixelmon:aggressive, pixelmon:advanced, pixelmon:raid. Unknown values fall back to random with a warning.
  • bossTier: notboss, common, uncommon, rare, epic, legendary, ultimate, drowned, haunted, equal.
  • Mega Evolution: give the Pokemon its stone as heldItem and set "canMega": true on the trainer. Leave form null, or it starts the battle already Mega.