BuffDefinition
Immutable configuration loaded from JSON
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:
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:
config/journey/buffs/The BuffInstance class represents an active buff:
The BuffTypeRegistry uses factories to create definitions from JSON:
"attribute", "potion", etc.) to factoriesLet’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.BuffDefinitionimport aster.amo.journey.buff.BuffInstanceimport 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 ) }}package com.example.cobbledaycare.buff;
import aster.amo.journey.buff.BuffDefinition;import aster.amo.journey.buff.BuffInstance;import net.minecraft.resources.ResourceLocation;
public class DaycareSpeedBuffDefinition extends BuffDefinition { private final double speedMultiplier;
public DaycareSpeedBuffDefinition( ResourceLocation id, String name, String description, int maxStacks, boolean isDebuff, boolean hidden, double speedMultiplier ) { super(id, name, description, maxStacks, isDebuff, hidden); this.speedMultiplier = speedMultiplier; }
@Override public BuffInstance createInstance(int duration, int amplifier, String source) { // Amplifier increases speed: level 0 = 1.5x, level 1 = 2.0x, level 2 = 2.5x double actualMultiplier = speedMultiplier + (amplifier * 0.5);
return new DaycareSpeedBuffInstance( getId(), duration, amplifier, source, actualMultiplier ); }
public double getSpeedMultiplier() { return speedMultiplier; }}Key Points:
BuffDefinition base classspeedMultiplier)createInstance() to create your BuffInstancepackage com.example.cobbledaycare.buff
import aster.amo.journey.buff.BuffInstanceimport aster.amo.journey.buff.BuffDefinitionimport aster.amo.journey.buff.RemovalReasonimport net.minecraft.nbt.CompoundTagimport net.minecraft.network.chat.Componentimport net.minecraft.resources.ResourceLocationimport net.minecraft.server.level.ServerPlayerimport 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") ) } }}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;
public class DaycareSpeedBuffInstance extends BuffInstance { private final double speedMultiplier;
public DaycareSpeedBuffInstance( ResourceLocation definitionId, UUID uuid, int duration, int amplifier, int remainingTicks, String source, double speedMultiplier ) { super(definitionId, uuid, duration, amplifier, remainingTicks, source); this.speedMultiplier = speedMultiplier; }
// Convenience constructor for new instances public DaycareSpeedBuffInstance( ResourceLocation definitionId, int duration, int amplifier, String source, double speedMultiplier ) { this(definitionId, UUID.randomUUID(), duration, amplifier, duration, source, speedMultiplier); }
@Override public void onApply(ServerPlayer player, BuffDefinition definition) { int percent = (int) ((speedMultiplier - 1.0) * 100); player.sendSystemMessage( Component.literal("Daycare speed increased by " + percent + "%!") ); }
@Override public void onTick(ServerPlayer player, BuffDefinition definition) { super.onTick(player, definition); // Your daycare system can query this buff's multiplier // and apply it to egg hatching calculations }
@Override public void onRemove(ServerPlayer player, BuffDefinition definition, RemovalReason reason) { switch (reason) { case EXPIRED: player.sendSystemMessage(Component.literal("Daycare speed buff expired")); break; case MANUAL: player.sendSystemMessage(Component.literal("Daycare speed buff removed")); break; default: break; } }
@Override public CompoundTag toNbt() { CompoundTag tag = super.toNbt(); tag.putDouble("speedMultiplier", speedMultiplier); return tag; }
public static DaycareSpeedBuffInstance fromNbt(CompoundTag tag) { return new DaycareSpeedBuffInstance( ResourceLocation.parse(tag.getString("definitionId")), tag.getUUID("uuid"), tag.getInt("duration"), tag.getInt("amplifier"), tag.getInt("remainingTicks"), tag.contains("source") ? tag.getString("source") : null, tag.getDouble("speedMultiplier") ); }
public double getSpeedMultiplier() { return speedMultiplier; }}Key Points:
BuffInstance base classonApply(), onTick(), onRemove()toNbt() to save custom propertiesfromNbt() companion method for loadingsuper.onTick() to handle duration countdownIn your mod’s initialization:
package com.example.cobbledaycare
import aster.amo.journey.buff.BuffTypeRegistryimport aster.amo.journey.buff.BuffRegistryimport 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:
FabricModInitializer.onInitialize()BuffRegistry.init()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.getimport aster.amo.journey.data.JourneyDataObjectimport com.example.cobbledaycare.buff.DaycareSpeedBuffInstanceimport 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:
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:
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:
Removal Reasons:
EXPIRED - Duration ran outMANUAL - Removed by command/APIDEATH - Player diedSOURCE_REMOVED - Source (e.g., levelable) was removedREPLACED - Replaced by another buffExample:
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:
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)}onRemove()onTick() lightweightregisterSafe() if other mods might register same type// Register a new buff typeBuffTypeRegistry.register( typeName: String, factory: (ResourceLocation, JsonObject) -> BuffDefinition)
// Register without throwing on duplicateBuffTypeRegistry.registerSafe( typeName: String, factory: (ResourceLocation, JsonObject) -> BuffDefinition): Boolean
// Check if type is registeredBuffTypeRegistry.isRegistered(typeName: String): Boolean
// Get all registered typesBuffTypeRegistry.getRegisteredTypes(): Set<String>// Register instance deserializerBuffRegistry.registerInstanceType( className: String, deserializer: (CompoundTag) -> BuffInstance)
// Apply buff to playerBuffRegistry.applyToPlayer( player: ServerPlayer, definitionId: ResourceLocation, duration: Int, amplifier: Int = 0, source: String? = null): BuffInstance?
// Get buff definitionBuffRegistry.getDefinition(id: ResourceLocation): BuffDefinition?// Apply buff to playerBuffManager.applyBuff( player: ServerPlayer, buffInstance: BuffInstance): Boolean
// Remove specific buffBuffManager.removeBuff( player: ServerPlayer, buffUuid: UUID, reason: RemovalReason)
// Remove all buffs of a typeBuffManager.removeBuffsByDefinition( player: ServerPlayer, definitionId: ResourceLocation, reason: RemovalReason)
// Remove all buffs from a sourceBuffManager.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:
// Definitionclass 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) }}
// Instanceclass 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 }}
// Registrationfun 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:
Problem: Buff disappears on server restart/player logout
Solutions:
toNbt() saves all custom propertiesfromNbt() loads all custom propertiesProblem: Event-driven buff doesn’t work
Solutions:
Problem: Server memory usage grows over time
Solutions:
onRemove()Now that you understand custom buff types, explore: