Effects
In this guide we'll implement a few simple effects to give an overview of the features and possibilities of effects.
We'll also demonstrate how to use the EffectFactory
to create instances of effects, configure them, and apply them to
characters.
What Can Effects Do?¶
The most simple example of effects would be attribute changes, like healing health over time, or increasing a character's
strength. However, effects in Ranvier are not simple buffs and debuffs; they receive all of the events the target
receives in addition to a special event called updateTick
, which we'll go into detail about later.
Some examples of how effects can be combined with other aspects of the Ranvier engine with interesting results:
- A trapped chest that shoots a poison needle into the player character when opened, dealing 'poison' damage over time unless the player drinks an antidote.
- A weapon that causes the player to be healed for a percentage of damage dealt to foes.
- A multiplier to experience points when grouped with other players.
- Boots that make a warrior's Kick skill do 20% more damage.
- A silence effect, preventing the player from using certain channels or casting certain spells.
- Cause the player character to have prophetic dreams while sleeping.
- A confusion spell causing a character to
say
gibberish every 30 seconds andmove
in random directions.
You can be creative when using effects, since they are as fully scriptable as commands or events. In fact, as mentioned above, you have access to all player commands and events via the effects system.
What is an Effect, exactly?¶
In Ranvier an Effect
is an object tied to a Character
(a player or NPC) by way of the Character's EffectList
. An Effect:
- has a duration (can potentially be permanent)
- can persist across log in/out
- Optionally runs some code when first activated
- Optionally runs some code when deactivated
- Optionally runs some code every "tick" (we'll describe
updateTick
later) - Optionally modifies incoming and outgoing damage
We'll cover the configuration of functionality as we implement some demonstrative effects.
Creating an Effect¶
Effects, similar to commands, are each stored in their own .js
file. In the case of effects it is in the effects/
folder underneath your bundle directory. In our example we'll be implementing buff
, damageshield
, regen
, and rend
effects, so our bundle folder would look like so:
bundles/my-effects/ effects/ buff.js damageshield.js regen.js rend.js
File Structure¶
Similar to all bundle-loaded .js
files the effect file will an object representing the definition of the effect. We'll
go over the definition of the effect in detail as we work through example effects. Below is the bare minimum you need
for an effect:
'use strict'; module.exports = { config: { name: 'My Effect', } };
Example Effects¶
These are brief overviews of different types of effects, there is more functionality not
on display here such as effect stacking and hidden effects so it's suggested that you read
over src/Effect.js
to take full advantage.
buff¶
This example buff will demonstrate a simple temporary attribute buff which increases the target's strength by an amount configurable by whatever is instantiating this effect.
bundles/my-effects/effects/buff.js 'use strict'; const { Broadcast, EffectFlag } = require('ranvier'); module.exports = { config: { // Name of effect shown when the player uses the `effects` command name: 'Buff Strength', description: "You feel stronger!", // Optional duration of this effect in milliseconds. Defaults to Infinity duration: 30 * 1000, /* Type is an optional config which is used in conjunction with the `unique` config option (defaults to true). If an effect is unique only one effect of that type may be active at once. */ type: 'buff.strength', /** * This will configure the effect so that if another effect of the same * type is applied before the effect is finished it will receive a * "effectRefreshed" event to do with as it will. */ refreshes: true, }, /* Effect flags are completely optional and _arbitrary_ values that you can place in the `flags` array and then read later. By default flags are only used by the `bundle-example-effects` bundle's `effects` command to color an active effect red or green. You can import flags from anywhere you want or simply hard code strings. The EffectFlag enum from src/ is just an _example_ implementation. */ flags: [EffectFlag.BUFF], /* State, like quest state, is where you keep track of the current state of the effect. This may include things like how many stacks of this effect there are, the magnitude of an effect, etc. In buff effect a magnitude of 5 indicates that we want to increase the target's attribute by 5 */ state: { magnitude: 5 }, /* The modifiers property is where you implement formulas for changing character attributes as well as incoming/outgoing damage. */ modifiers: { /* The attributes sub-property lets you define which attributes are modified by this effect. */ attributes: { // For `buff` we just want to take the character's current strength and // increase it by this effect's `magnitude` strength: function (current) { return current + this.state.magnitude; } } }, /* Alternatively, if the attribute you're modifying is dynamic you can use this pattern which is used when you want a base effect that could apply to multiple attributes. See the `equip.js` effect for an example state: { stat: 'strength', bonus: 5 }, modifiers: { attributes: function (attribute, current) { if (attribute !== this.state.stat) { return current; } return current + this.state.bonus; } }, */ /* Similar to quests, effects receive all the events the player receives in addition to a few special events specific to events. The special events are: effectAdded: The effect has been added to the character's effect list but is not yet activated. effectActivated: The effect is activated for the character effectDeactivated: The effect is about to be removed from the effect list */ listeners: { effectRefreshed: function (newEffect) { // For this buff if someone tries to refresh the effect then just restart // the duration timer this.startedAt = Date.now(); Broadcast.sayAt(this.target, "You refresh the potion's magic."); }, effectActivated: function () { // For buff we'll just send some text to the user Broadcast.sayAt(this.target, "Strength courses through your veins!"); }, effectDeactivated: function () { Broadcast.sayAt(this.target, "You feel weaker."); } } };
regen¶
This regen effect demonstrates a "ticking" effect, i.e., an effect which has some logic that runs every N seconds. This implementation is used as the constant out-of-combat healing for the player
bundles/my-effects/effects/regen.js 'use strict'; // This effect is going to heal the player so need to use a Heal object for that const { Broadcast, EffectFlag, Heal } = require('ranvier'); module.exports = { config: { name: 'Regenerate Health', description: "You are regenerating health over time.", type: 'regen.health', // tickInterval defines how many seconds between consecutive `updateTick` events // Note: This is _seconds_, not _milliseconds_ which is duration tickInterval: 3 }, flags: [EffectFlag.BUFF], state: { magnitude: 10, }, listeners: { effectedAdded: function () { // this is out-of-combat regen, remove this effect if they are in combat if (this.target.isInCombat()) { this.remove(); } }, /* `updateTick` is a special event that fires on all entities in the game every .5 seconds. With the `tickInterval` config option above, however, it will only trigger this function every `ticketInterval` seconds. */ updateTick: function () { const start = this.target.getAttribute('health'); const max = this.target.getMaxAttribute('health'); if (start >= max) { // once the character has reached max health remove the effect return this.remove(); } // heal them for this effect's magnitude amount const heal = new Heal('health', this.state.magnitude, this.target, this, { hidden: true, }); heal.commit(this.target); }, // this event is just a general Character event and is fired when a character enters combat combatStart: function () { this.remove(); } } };
damageshield¶
The damageshield effect will demonstrate modifying incoming damage for the affected target. Damage shield will be an effect which absorbs incoming damage of a certain type up to a certain amount and then disappear once either A) expired, or B) depleted.
bundles/my-effects/effects/damageshield.js 'use strict'; const { Broadcast, EffectFlag, Heal, Player } = require('ranvier'); module.exports = { config: { name: 'Damage Shield', description: "You are temporarily protected from damage!", type: 'shield', }, flags: [EffectFlag.BUFF], state: { magnitude: 50, remaining: 50, type: "physical" }, modifiers: { /* the incomingDamage modifier, and its sibling property outgoingDamage, let you do what it says on the tin. The function takes the Damage object (see `src/Damage.js` for more detail) and the current amount of damage about to be dealt. */ incomingDamage: function (damage, currentAmount) { // In our shield effect we don't want to absorb heals and we don't want to absorb // damage to stats other than health if (damage instanceof Heal || damage.attribute !== 'health') { return currentAmount; } // Absorb incoming damage const absorbed = Math.min(this.state.remaining, currentAmount); this.state.remaining -= absorbed; currentAmount -= absorbed; Broadcast.sayAt(this.target, `Your damage shield absorbs <bold>${absorbed}</bold> damage!`); // the shield is depleted, remove the effect if (!this.state.remaining) { this.remove(); } // return the remaining amount of damage return currentAmount; } }, listeners: { effectActivated: function () { Broadcast.sayAt(this.target, `A shield of energy shield envelops you, protecting you from harm!`); if (this.target instanceof Player) { // this damageshield effect does something kind of cool, it adds an extra prompt // to the player's prompt lines showing the remaining shield amount as a // progress bar with the `addPrompt()` function this.target.addPrompt("damageshield", () => { const width = 60 - "Shield".length; const remaining = `<bold>${this.state.remaining}/${this.state.magnitude}</bold>`; return "<bold>Shield:</bold> " + Broadcast.progress(width, (this.state.remaining / this.state.magnitude) * 100, "cyan") + ` ${remaining}`; }); } }, effectDeactivated: function () { Broadcast.sayAt(this.target, "The shield of energy around you dissipates."); if (this.target instanceof Player) { // remove the shield's remaining amount meter prompt this.target.removePrompt("damageshield"); } } } };
rend¶
This rend effect is an example of a damage over time effect which could be used by a Rend skill. As the "damage over time" name implies this type of effect does some damage over a set amount of time. This example effect will also demonstrate stacking effects, which are effects that, when another effect of the same type attempts to be applied, increments a stack counter and can run some code as we'll see below.
bundles/my-effects/effects/skill.rend.js 'use strict'; const { Broadcast, Damage, EffectFlag } = require('ranvier'); /** * Implementation effect for a Rend damage over time skill */ module.exports = { config: { name: 'Rend', duration: 15 * 1000, type: 'skill:rend', tickInterval: 3, // Sets the max number of stacks that can be added to this effect once active maxStacks: 3, }, flags: [EffectFlag.DEBUFF], listeners: { // this is the event that gets fired when a new stack gets added. `newEffect` is // the effect that was attempted to be added, provided so you can do things like compound // damage or refresh the duration, etc. effectStackAdded: function (newEffect) { // add incoming rend's damage to the existing damage but don't extend duration this.state.totalDamage += newEffect.state.totalDamage; }, effectActivated: function () { Broadcast.sayAt(this.target, "<bold><red>You've suffered a deep wound, it's bleeding profusely</red></bold>"); }, effectDeactivated: function () { Broadcast.sayAt(this.target, "Your wound has stopped bleeding."); }, updateTick: function () { const amount = Math.round(this.state.totalDamage / Math.round((this.config.duration / 1000) / this.config.tickInterval)); // as seen with Skills we'll create a new damage object and commit that damage to the target // of this effect const damage = new Damage({ attribute: "health", amount, attacker: this.attacker, source: this }); damage.commit(this.target); }, // remove this rend effect when the target dies killed: function () { this.remove(); } } };
Using Effects¶
Above we've implemented new effect types, now we'll actually use those effects during gameplay.
For our buff effect we're going to create a simple skill called "enrage" that uses our effect. More details on creating skills can be found in the Skills section of the guide.
'use strict'; const { Broadcast, SkillType } = require('ranvier'); module.exports = { name: 'Enrage', type: SkillType.SKILL, resource: { attribute: 'energy', cost: 30 }, cooldown: 30 run: state => function (args, player, target) { if (!player.isInCombat(target)) { return Broadcast.sayAt(player, "You're not fighting them at the moment."); } // To create an instance of an effect you use the EffectFactory const effect = state.EffectFactory.create( 'buff', // specifying the effect type (name of the effect file minus .js) /* a config override, in this case set a duration of 15 seconds instead of the default 30 */ { duration: 15 * 1000 }, /* and a state override. In this example we'll override the default 5 strength increase to instead increase the player's strength by 10% */ { magnitude: Math.floor(player.getAttribute('strength') / 10) } ); // these are special properties of the effect used during combat effect.skill = this; effect.attacker = player; Broadcast.sayAt(player, `<red>You let out a gutteral roar and your vision goes red!</red>`); // Finally add the effect to the character. Our effect is autoActivated so we don't // have to worry about activating it manually player.addEffect(effect); }, info: (player) => { return 'Temporarily increase your strength.'; } };
The same pattern is used for all effects we've implemented: Use EffectFactory.create
to
create a new instance of the effect with any overrides you may want then call
addEffect(yourEffect)
on the target character.
Accessing GameState¶
An example could be that you have an effect on a player that causes any healing done to provide an additional 50%
healing as a heal-over-time effect. To achieve this the listeners
config can also be defined as a function.
Standard¶
module.exports = { // ... listeners: { heal: function () {}, }, };
With GameState¶
module.exports = { // ... // listeners here is a function with one argument, the GameState, and // returning the standard listeners object listeners: state => ({ heal: function () {}, }), };
Example¶
'use strict'; const { Broadcast, EffectFlag } = require('ranvier'); /** * Implementation effect for a Rend damage over time skill */ module.exports = { config: { name: 'Bonus Healing', type: 'skill:bonushealing' }, flags: [EffectFlag.BUFF], // instead of a standard `listeners: { /* ... /* }` object, `listeners` can be // a function accepting GameState as an argument and returns the normal object listeners: state => ({ heal: function (heal, target, finalAmount) { const newHealAmount = Math.floor(finalAmount / 2); const hotEffect = state.EffectFactory.create( 'bonus-hot', { duration: 5000 }, { amount: newHealAmount }, ); target.addEffect(hotEffect); } }) };
Further Reading¶
Effects really are where the interesting pieces of the engine come together. You can see
some example implementation of more effects in the bundle-example-classes
bundle.