Skip to content

Custom Buff Types (For Developers)

Advanced For Mod Developers Only

Journey’s buff system is fully extensible, allowing third-party mods to register custom buff types through the BuffTypeRegistry API. This enables you to create buffs with completely custom behavior that integrates seamlessly with Journey’s task and levelable reward systems.

Before you begin, you should have:

  • A Fabric mod project set up with Kotlin support
  • Journey mod as a dependency in your project
  • Basic understanding of Kotlin and Fabric mod development
  • Familiarity with NBT (Named Binary Tag) for data serialization
  • Knowledge of Minecraft’s event system (helpful but not required)

Creating a custom buff type involves three main components:

BuffDefinition

Immutable configuration loaded from JSON

BuffInstance

Runtime state and behavior for active buffs

Registration

Register your type with BuffTypeRegistry

The BuffDefinition class represents the immutable configuration:

  • Loaded from JSON files in config/journey/buffs/
  • Contains all buff properties
  • Creates BuffInstance objects when buff is applied

The BuffInstance class represents an active buff:

  • Tracks runtime state (remaining duration, etc.)
  • Handles lifecycle events (apply, tick, remove)
  • Persists to NBT for saving/loading
  • Can access player and perform actions

The BuffTypeRegistry uses factories to create definitions from JSON:

  • Maps type strings ("attribute", "potion", etc.) to factories
  • Factory receives parsed JSON and returns BuffDefinition
  • Allows arbitrary JSON structure for your buff type

Let’s create a buff that speeds up Pokémon egg hatching in a daycare system.

package com.example.cobbledaycare.buff
import aster.amo.journey.buff.BuffDefinition
import aster.amo.journey.buff.BuffInstance
import net.minecraft.resources.ResourceLocation
class DaycareSpeedBuffDefinition(
id: ResourceLocation,
name: String?,
description: String?,
maxStacks: Int,
isDebuff: Boolean,
hidden: Boolean,
val speedMultiplier: Double
) : BuffDefinition(id, name, description, maxStacks, isDebuff, hidden) {
override fun createInstance(
duration: Int,
amplifier: Int,
source: String?
): BuffInstance {
// Amplifier increases speed: level 0 = 1.5x, level 1 = 2.0x, level 2 = 2.5x
val actualMultiplier = speedMultiplier + (amplifier * 0.5)
return DaycareSpeedBuffInstance(
definitionId = id,
duration = duration,
amplifier = amplifier,
source = source,
speedMultiplier = actualMultiplier
)
}
}

Key Points:

  • Extend BuffDefinition base class
  • Pass common properties to super constructor
  • Add custom properties (e.g., speedMultiplier)
  • Implement createInstance() to create your BuffInstance
  • Use amplifier to scale your effect
package com.example.cobbledaycare.buff
import aster.amo.journey.buff.BuffInstance
import aster.amo.journey.buff.BuffDefinition
import aster.amo.journey.buff.RemovalReason
import net.minecraft.nbt.CompoundTag
import net.minecraft.network.chat.Component
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import java.util.UUID
class DaycareSpeedBuffInstance(
definitionId: ResourceLocation,
uuid: UUID = UUID.randomUUID(),
duration: Int = -1,
amplifier: Int = 0,
remainingTicks: Int = duration,
source: String? = null,
val speedMultiplier: Double
) : BuffInstance(definitionId, uuid, duration, amplifier, remainingTicks, source) {
override fun onApply(player: ServerPlayer, definition: BuffDefinition) {
player.sendSystemMessage(
Component.literal("Daycare speed increased by ${((speedMultiplier - 1.0) * 100).toInt()}%!")
)
}
override fun onTick(player: ServerPlayer, definition: BuffDefinition) {
super.onTick(player, definition)
// Your daycare system can query this buff's multiplier
// and apply it to egg hatching calculations
}
override fun onRemove(player: ServerPlayer, definition: BuffDefinition, reason: RemovalReason) {
when (reason) {
RemovalReason.EXPIRED -> player.sendSystemMessage(
Component.literal("Daycare speed buff expired")
)
RemovalReason.MANUAL -> player.sendSystemMessage(
Component.literal("Daycare speed buff removed")
)
else -> {}
}
}
override fun toNbt(): CompoundTag {
val tag = super.toNbt()
tag.putDouble("speedMultiplier", speedMultiplier)
return tag
}
companion object {
fun fromNbt(tag: CompoundTag): DaycareSpeedBuffInstance {
return DaycareSpeedBuffInstance(
definitionId = ResourceLocation.parse(tag.getString("definitionId")),
uuid = tag.getUUID("uuid"),
duration = tag.getInt("duration"),
amplifier = tag.getInt("amplifier"),
remainingTicks = tag.getInt("remainingTicks"),
source = if (tag.contains("source")) tag.getString("source") else null,
speedMultiplier = tag.getDouble("speedMultiplier")
)
}
}
}

Key Points:

  • Extend BuffInstance base class
  • Implement lifecycle methods: onApply(), onTick(), onRemove()
  • Override toNbt() to save custom properties
  • Create fromNbt() companion method for loading
  • Call super.onTick() to handle duration countdown

In your mod’s initialization:

package com.example.cobbledaycare
import aster.amo.journey.buff.BuffTypeRegistry
import aster.amo.journey.buff.BuffRegistry
import net.minecraft.resources.ResourceLocation
object CobbleDaycare {
fun init() {
// Register your buff type with Journey
BuffTypeRegistry.register("daycare_speed") { id, jsonObject ->
DaycareSpeedBuffDefinition(
id = id,
name = jsonObject.get("name")?.asString,
description = jsonObject.get("description")?.asString,
maxStacks = jsonObject.get("max_stacks")?.asInt ?: 1,
isDebuff = jsonObject.get("is_debuff")?.asBoolean ?: false,
hidden = jsonObject.get("hidden")?.asBoolean ?: false,
speedMultiplier = jsonObject.get("speed_multiplier")?.asDouble ?: 1.5
)
}
// Register instance deserializer for NBT loading
BuffRegistry.registerInstanceType(
DaycareSpeedBuffInstance::class.java.name
) { tag -> DaycareSpeedBuffInstance.fromNbt(tag) }
}
}

Registration Timing:

  • Call during FabricModInitializer.onInitialize()
  • Must be before Journey’s BuffRegistry.init()
  • Use Fabric’s mod loading order if needed

Users can now create buff configs using your type:

File: config/journey/buffs/daycare_boost.json

{
"id": "daycare_boost",
"type": "daycare_speed",
"name": "Daycare Speed Boost",
"description": "Pokémon eggs hatch faster in the daycare",
"max_stacks": 3,
"is_debuff": false,
"hidden": false,
"speed_multiplier": 1.5
}

Query active buffs in your daycare system:

import aster.amo.ceremony.utils.extension.get
import aster.amo.journey.data.JourneyDataObject
import com.example.cobbledaycare.buff.DaycareSpeedBuffInstance
import net.minecraft.resources.ResourceLocation
fun calculateEggHatchSpeed(player: ServerPlayer): Double {
val data = player.get<JourneyDataObject>()
val daycareBuffId = ResourceLocation.fromNamespaceAndPath("journey", "daycare_boost")
var totalMultiplier = 1.0
// Sum all daycare speed buffs
data.activeBuffs
.filter { it.definitionId == daycareBuffId }
.filterIsInstance<DaycareSpeedBuffInstance>()
.forEach { totalMultiplier += (it.speedMultiplier - 1.0) }
return totalMultiplier
}

Called once when buff is first applied to the player.

Use Cases:

  • Send notification to player
  • Apply initial effect
  • Subscribe to events
  • Initialize state

Example:

override fun onApply(player: ServerPlayer, definition: BuffDefinition) {
player.sendSystemMessage(Component.literal("Buff applied!"))
// Apply attribute modifier
val attribute = player.getAttribute(Attributes.MOVEMENT_SPEED)
attribute?.addPermanentModifier(
AttributeModifier(uuid, "buff_modifier", 0.2, AttributeModifier.Operation.ADD_MULTIPLIED_BASE)
)
}

Called every game tick (20 times per second) while buff is active.

Use Cases:

  • Periodic effects
  • Continuous calculations
  • Update visual effects
  • Check conditions

Important: Always call super.onTick() to handle duration countdown!

Example:

override fun onTick(player: ServerPlayer, definition: BuffDefinition) {
super.onTick(player, definition) // Required!
// Heal 0.5 HP every 20 ticks (1 second)
if (remainingTicks % 20 == 0) {
player.heal(0.5f)
}
}

Called once when buff is removed for any reason.

Use Cases:

  • Clean up effects
  • Send notification
  • Unsubscribe from events
  • Remove attribute modifiers

Removal Reasons:

  • EXPIRED - Duration ran out
  • MANUAL - Removed by command/API
  • DEATH - Player died
  • SOURCE_REMOVED - Source (e.g., levelable) was removed
  • REPLACED - Replaced by another buff

Example:

override fun onRemove(player: ServerPlayer, definition: BuffDefinition, reason: RemovalReason) {
when (reason) {
RemovalReason.EXPIRED ->
player.sendSystemMessage(Component.literal("Buff expired!"))
RemovalReason.DEATH ->
player.sendSystemMessage(Component.literal("Buff lost on death"))
else -> {}
}
// Clean up attribute modifier
val attribute = player.getAttribute(Attributes.MOVEMENT_SPEED)
attribute?.removeModifier(uuid)
}

Called when player logs in/out while buff is active.

Use Cases:

  • Reapply effects that don’t persist (like attributes)
  • Update state
  • Clean up temporary effects

Example:

override fun onLogin(player: ServerPlayer, definition: BuffDefinition) {
// Reapply attribute modifier (attributes don't persist)
val attribute = player.getAttribute(Attributes.MOVEMENT_SPEED)
attribute?.addPermanentModifier(
AttributeModifier(uuid, "buff_modifier", 0.2, AttributeModifier.Operation.ADD_MULTIPLIED_BASE)
)
}

All buff instances must be able to save/load from NBT.

Override toNbt() to save custom properties:

override fun toNbt(): CompoundTag {
val tag = super.toNbt() // Save base properties
// Save your custom properties
tag.putDouble("customValue", customValue)
tag.putString("customString", customString)
tag.putBoolean("customFlag", customFlag)
return tag
}

Create a companion fromNbt() method:

companion object {
fun fromNbt(tag: CompoundTag): MyCustomBuffInstance {
return MyCustomBuffInstance(
// Load base properties
definitionId = ResourceLocation.parse(tag.getString("definitionId")),
uuid = tag.getUUID("uuid"),
duration = tag.getInt("duration"),
amplifier = tag.getInt("amplifier"),
remainingTicks = tag.getInt("remainingTicks"),
source = if (tag.contains("source")) tag.getString("source") else null,
// Load custom properties
customValue = tag.getDouble("customValue"),
customString = tag.getString("customString"),
customFlag = tag.getBoolean("customFlag")
)
}
}

Register your fromNbt() method with BuffRegistry:

BuffRegistry.registerInstanceType(
MyCustomBuffInstance::class.java.name
) { tag -> MyCustomBuffInstance.fromNbt(tag) }

For buffs that respond to game events (like shiny chance buff):

class MyEventBuffInstance(...) : BuffInstance(...) {
companion object {
private var eventSubscribed = false
fun subscribeToEvent() {
if (eventSubscribed) return
eventSubscribed = true
SomeGameEvent.EVENT.register { event ->
val player = event.player as? ServerPlayer ?: return@register
val data = player.get<JourneyDataObject>()
// Find all active instances of this buff
val myBuffs = data.activeBuffs
.filterIsInstance<MyEventBuffInstance>()
// Apply cumulative effect
var totalEffect = 0.0
myBuffs.forEach { buff ->
totalEffect += buff.effectValue
}
// Modify event
event.addModifier(totalEffect)
}
}
}
}
fun init() {
BuffTypeRegistry.register("my_event_type") { id, json ->
// ... create definition
}
BuffRegistry.registerInstanceType(
MyEventBuffInstance::class.java.name
) { tag -> MyEventBuffInstance.fromNbt(tag) }
// Subscribe to event once
MyEventBuffInstance.subscribeToEvent()
}

Override canStack() and onStack() to customize stacking:

override fun canStack(other: BuffInstance): Boolean {
// Only stack if both are from same source
return this.source == other.source
}
override fun onStack(newInstance: BuffInstance) {
// Add new duration to existing instead of replacing
this.remainingTicks += newInstance.remainingTicks
// Don't update amplifier
// (default behavior would update it)
}
  • Avoid storing large objects in BuffInstance
  • Clean up resources in onRemove()
  • Unsubscribe from events when appropriate
  • Keep onTick() lightweight
  • Use tick intervals for expensive operations
  • Cache calculations when possible
  • Avoid iterating all players/entities every tick
  • Validate JSON properties in factory
  • Provide sensible defaults for missing properties
  • Log warnings for invalid configurations
  • Don’t crash if player/entity is invalid
  • Use registerSafe() if other mods might register same type
  • Check if Journey APIs exist before using
  • Don’t assume specific Journey version features
  • Document required Journey version
// Register a new buff type
BuffTypeRegistry.register(
typeName: String,
factory: (ResourceLocation, JsonObject) -> BuffDefinition
)
// Register without throwing on duplicate
BuffTypeRegistry.registerSafe(
typeName: String,
factory: (ResourceLocation, JsonObject) -> BuffDefinition
): Boolean
// Check if type is registered
BuffTypeRegistry.isRegistered(typeName: String): Boolean
// Get all registered types
BuffTypeRegistry.getRegisteredTypes(): Set<String>
// Register instance deserializer
BuffRegistry.registerInstanceType(
className: String,
deserializer: (CompoundTag) -> BuffInstance
)
// Apply buff to player
BuffRegistry.applyToPlayer(
player: ServerPlayer,
definitionId: ResourceLocation,
duration: Int,
amplifier: Int = 0,
source: String? = null
): BuffInstance?
// Get buff definition
BuffRegistry.getDefinition(id: ResourceLocation): BuffDefinition?
// Apply buff to player
BuffManager.applyBuff(
player: ServerPlayer,
buffInstance: BuffInstance
): Boolean
// Remove specific buff
BuffManager.removeBuff(
player: ServerPlayer,
buffUuid: UUID,
reason: RemovalReason
)
// Remove all buffs of a type
BuffManager.removeBuffsByDefinition(
player: ServerPlayer,
definitionId: ResourceLocation,
reason: RemovalReason
)
// Remove all buffs from a source
BuffManager.removeBuffsBySource(
player: ServerPlayer,
source: String,
reason: RemovalReason
)
abstract class BuffDefinition(
val id: ResourceLocation,
val name: String?,
val description: String?,
val maxStacks: Int,
val isDebuff: Boolean,
val hidden: Boolean
) {
abstract fun createInstance(
duration: Int,
amplifier: Int,
source: String?
): BuffInstance
}
abstract class BuffInstance(
val definitionId: ResourceLocation,
val uuid: UUID,
val duration: Int,
var amplifier: Int,
var remainingTicks: Int,
val source: String?
) {
val isPermanent: Boolean get() = duration == -1
val isExpired: Boolean get() = !isPermanent && remainingTicks <= 0
open fun onApply(player: ServerPlayer, definition: BuffDefinition) {}
open fun onTick(player: ServerPlayer, definition: BuffDefinition) {}
open fun onRemove(player: ServerPlayer, definition: BuffDefinition, reason: RemovalReason) {}
open fun onLogin(player: ServerPlayer, definition: BuffDefinition) {}
open fun onLogout(player: ServerPlayer, definition: BuffDefinition) {}
open fun canStack(other: BuffInstance): Boolean = true
open fun onStack(newInstance: BuffInstance) {
this.remainingTicks = newInstance.remainingTicks
this.amplifier = newInstance.amplifier
}
open fun toNbt(): CompoundTag
}

A complete example of a Cobblemon-integrated buff that boosts stats in battle:

// Definition
class BattleStatBuffDefinition(
id: ResourceLocation,
name: String?,
description: String?,
maxStacks: Int,
isDebuff: Boolean,
hidden: Boolean,
val stat: String,
val stages: Int
) : BuffDefinition(id, name, description, maxStacks, isDebuff, hidden) {
override fun createInstance(duration: Int, amplifier: Int, source: String?): BuffInstance {
return BattleStatBuffInstance(id, duration, amplifier, source, stat, stages)
}
}
// Instance
class BattleStatBuffInstance(
definitionId: ResourceLocation,
uuid: UUID = UUID.randomUUID(),
duration: Int = -1,
amplifier: Int = 0,
remainingTicks: Int = duration,
source: String? = null,
val stat: String,
val stages: Int
) : BuffInstance(definitionId, uuid, duration, amplifier, remainingTicks, source) {
companion object {
private var subscribed = false
fun subscribe() {
if (subscribed) return
subscribed = true
CobblemonEvents.BATTLE_STARTED_POST.subscribe { event ->
event.battle.actors.forEach { actor ->
val player = actor.entity as? ServerPlayer ?: return@forEach
val data = player.get<JourneyDataObject>()
val statBuffs = data.activeBuffs
.filterIsInstance<BattleStatBuffInstance>()
statBuffs.forEach { buff ->
val totalStages = buff.stages * (buff.amplifier + 1)
// Apply stat stages to battle Pokemon
actor.pokemonList.forEach { battlePokemon ->
when (buff.stat) {
"attack" -> battlePokemon.effectedPokemon.incrementStat(Stats.ATTACK, totalStages)
"defense" -> battlePokemon.effectedPokemon.incrementStat(Stats.DEFENCE, totalStages)
"speed" -> battlePokemon.effectedPokemon.incrementStat(Stats.SPEED, totalStages)
// etc...
}
}
}
}
}
}
fun fromNbt(tag: CompoundTag): BattleStatBuffInstance {
return BattleStatBuffInstance(
definitionId = ResourceLocation.parse(tag.getString("definitionId")),
uuid = tag.getUUID("uuid"),
duration = tag.getInt("duration"),
amplifier = tag.getInt("amplifier"),
remainingTicks = tag.getInt("remainingTicks"),
source = if (tag.contains("source")) tag.getString("source") else null,
stat = tag.getString("stat"),
stages = tag.getInt("stages")
)
}
}
override fun toNbt(): CompoundTag {
val tag = super.toNbt()
tag.putString("stat", stat)
tag.putInt("stages", stages)
return tag
}
}
// Registration
fun init() {
BuffTypeRegistry.register("battle_stat") { id, json ->
BattleStatBuffDefinition(
id = id,
name = json.get("name")?.asString,
description = json.get("description")?.asString,
maxStacks = json.get("max_stacks")?.asInt ?: 1,
isDebuff = json.get("is_debuff")?.asBoolean ?: false,
hidden = json.get("hidden")?.asBoolean ?: false,
stat = json.get("stat")?.asString ?: "attack",
stages = json.get("stages")?.asInt ?: 1
)
}
BuffRegistry.registerInstanceType(
BattleStatBuffInstance::class.java.name
) { BattleStatBuffInstance.fromNbt(it) }
BattleStatBuffInstance.subscribe()
}

JSON Config:

{
"id": "battle_power",
"type": "battle_stat",
"name": "Battle Power",
"description": "Increases Attack in battles",
"max_stacks": 3,
"stat": "attack",
"stages": 2
}

Problem: Buff definition doesn’t load from JSON

Solutions:

  • Check type name matches exactly what you registered
  • Verify factory is registered before Journey’s init
  • Check server logs for JSON parsing errors
  • Validate JSON syntax

Problem: Buff disappears on server restart/player logout

Solutions:

  • Ensure toNbt() saves all custom properties
  • Verify fromNbt() loads all custom properties
  • Check deserializer is registered with correct class name
  • Look for NBT-related errors in logs

Problem: Event-driven buff doesn’t work

Solutions:

  • Ensure event subscription happens during init
  • Check event is the correct Fabric/Cobblemon event
  • Verify event handler logic is correct
  • Use logging to debug event firing

Problem: Server memory usage grows over time

Solutions:

  • Clean up resources in onRemove()
  • Don’t store references to players/entities
  • Unsubscribe from events when appropriate
  • Use weak references if needed

Now that you understand custom buff types, explore:

  • Integration with Cobblemon events
  • Creating buff reward types
  • Building buff-based progression systems
  • Combining buffs with Journey’s other features