Custom buffs

A resource can create its own buffs, with one of the game's status icons, a timer, stat changes and an optional tick. The server runs the buff, and the player sees it with its timer in their status list. Buffs.register has two forms.

FormBest for
Buffs.register(id, definition)full buffs with a GUID, stacks and modifiers, including the game's own stats
Buffs.register(definition)a simple buff owned by your resource, with an onTick callback

Full form

const { Buffs } = kcds.players;
const { Events } = kcds.core;

const blessing = Buffs.register('myres:blessing', {
  name: 'Blessing',
  description: 'A priest blessed you.',
  visual: 'well_rested',
  duration: 120,
  tickInterval: 10,
  stackable: true,
  maxStacks: 3,
  modifiers: { stamina: 20, health: { operation: 'add', value: 5 } },
});

Events.on('playerCustomBuffApply', (player, buff) => {
  console.info(`${buff.displayName} on ${player.nickname}`);
});
Events.on('playerCustomBuffTick', (player, buff) => {
  player.addStat('health', 1);
});
Events.on('playerCustomBuffRemove', (player, buff, reason) => {
  console.info(`${buff.name}: ${reason}`);
});

player.addBuff(blessing); // the GUID, or use the id

It returns the buff's GUID, which comes from the id and so is the same at each start. Registering the same definition under the same id again returns the same GUID. A different definition throws Custom buff myres:blessing is already registered with another definition.

FieldMeaning
id1 to 128 characters of A-Z a-z 0-9 : _ . -; not the name of a game buff
namerequired; the text the player sees, up to 256 bytes
descriptionoptional text, up to 4096 bytes
visualrequired; the game buff, by name or GUID, whose status icon it borrows
durationseconds, above 0 and up to 7 days; -1 (default) has no end
tickInterval0.1 to 86400 seconds; 0 (default) has no ticks
stackablewhether a player can have several instances; default false
maxStacks1 to 16 (default 1): how many instances a stackable buff keeps
refreshat the limit, true (default) replaces the oldest; false ignores the new one
modifiers{ stat: points } or { stat: { operation, value } }; Buffs.modifiers lists the stats

An unknown key, wrong value, or visual that is not a game buff throws Invalid custom buff: ....

Modifiers

OperationEffect
'add' (a number)adds the points
'multiply'multiplies the stat by value, 0 or more; 1.5 adds half again
'set'sets the stat to value

Modifiers work on three groups of stats.

Direct stats: health, stamina, exhaust and hunger. An instance works out its change from the player's last reported value when it lands, and takes back exactly that when it ends. stamina: { operation: 'multiply', value: 1.5 } on 50 stamina adds 25, then later takes 25 away. Three stacks of stamina: 20 give 60, and leave nothing behind.

Rates per second: healthRegeneration gives health, hungerDrain takes hunger, and energyDrain adds exhaust. These accept a number of 0 or more, or 'add', and apply each second while an instance lasts. What they changed stays changed.

The game's own stats: maxStamina, carryCapacity, staminaRegeneration, runSpeed, charisma, strength, agility, vitality, speech, and skills by track name, such as weapon_sword, fencing and stealth. These go through the game's buff system. The game applies them once, however many stacks there are, while the buff is on the status list. Derived values follow, so more strength lets a player carry more.

A custom buff cannot change maximum health or armor rating. The game has no buff modifier that raises them. Its buff table never changes them, and its mhs and bba codes moved neither stat when tested.

const { Buffs } = kcds.players;

Buffs.register('myres:giant', {
  name: 'Giant',
  visual: 'well_rested',
  duration: 600,
  modifiers: { maxStamina: { operation: 'multiply', value: 1.5 }, carryCapacity: 100, charisma: 5 },
});

Buffs.register('myres:marathon', {
  name: 'Marathon',
  visual: 'well_rested',
  duration: 600,
  modifiers: { hungerDrain: 0.2, energyDrain: 0.1 },
});

Buffs.register('myres:feast', {
  name: 'Feast',
  visual: 'well_rested',
  duration: 300,
  modifiers: { hunger: { operation: 'set', value: 100 }, healthRegeneration: 0.5 },
});

Events and reading

  • playerCustomBuffApply fires for each instance.
  • playerCustomBuffTick fires for each instance and interval.
  • playerCustomBuffRemove gives a reason: expired, server (from removeBuff, which takes all instances), refreshed (a new stack replaced the oldest) or disconnect.
  • addBuff returns false when the buff is at its limit and does not refresh.
  • In player.buffs, each instance has its own entry with custom: true, displayName and description. Game buffs have custom: false and null for those texts.

The buff these events receive is what Buffs.find returns:

{
  "name": "myres:blessing",
  "class": "custom",
  "aiTag": null,
  "duration": 120,
  "custom": true,
  "displayName": "Blessing",
  "description": "A priest blessed you."
}

Simple form

const { Buffs } = kcds.players;

const warmth = Buffs.register({
  name: 'warmth',
  label: 'Warm',
  icon: 'hangover',
  duration: 120,
  effects: { stats: { stamina: 20 }, tickStats: { health: 1 } },
  tickSeconds: 5,
  onTick: (player, buff) => console.info(player.nickname, buff.name),
});

kcds.core.Events.on('playerBuffRemoved', (player, buff, reason) => {
  if (buff === 'warmth') {
    kcds.core.Chat.sendToAll(`The warmth wore off (${reason})`);
  }
});

player.addBuff(warmth); // or 'warmth'
player.hasBuff(warmth);
player.removeBuff(warmth);
FieldMeaning
name1 to 64 characters of A-Z a-z 0-9 : _ . -; not a game buff or one already registered
iconthe name of a game buff, as Buffs.find spells it, whose status icon it uses
labelthe text in the status list; name by default
durationseconds, above 0 and up to 7 days; -1 has no end
effects.statsstat changes, as for player.addStat, applied each time it is added
effects.tickStatsstat changes on each tick
tickSecondsthe tick interval, at least 1 second; needs onTick or effects.tickStats
onTick(player, buff)called every tickSeconds while the buff lasts; missed ticks are not made up

It returns a frozen description, with name, icon, label, duration and tickSeconds, which player.addBuff, removeBuff and hasBuff take in place of the name. The buff belongs to your resource and goes when it stops, including from players.

How it behaves:

  • addBuff applies at once, so hasBuff is true straight away, and playerBuffAdded fires with the source server.
  • Adding a buff the player already has restarts its timer and applies its effects again, with no events.
  • When it ends, playerBuffRemoved fires with the reason expired, or server for removeBuff.
  • In player.buffs it has class: 'custom' and source: 'server'.
  • It ends when the player disconnects and does not carry over.

To decide the game's own effects on the server, see Buffs.claim in Buffs.