Plugin events Conteúdo

Plugins

Plugin events

Every moment a plugin can listen to: when it fires, what it carries, what a listener can change or cancel, priorities, and what happens in party fights.

Listening for a moment

A plugin listens with wt.on, inside onEnable: the name of a moment and a function to run. The function gets the event, e: who it is about (e.character), the event's own fields (e.place, e.amount), and for some events a way to change or stop what happens.

export default {
  onEnable(wt) {
    wt.on('place.enter', async (e) => {
      if (!e.place || !e.place.tags.includes('haunted')) return;
      await wt.gm.tell(e.character.name + ' feels the cold of a haunted place.');
    });
  },
};

A listener can wait on the world (await wt.places.get(...)); the turn waits for it, within the limits.

Priorities and cancelling

Each listener has a priority: lowest, low, normal (the default), high, highest or monitor. They run lowest first, so the highest has the last word, and each sees the event as the one before left it. Listeners at the same priority in different plugins run in the Plugins tab's run order.

  • Cancelling. On an event that can be cancelled, e.cancel() stops what would have happened (a blow, a roll, a death), and e.uncancel() lets it happen after all. Calling e.cancel() on any other event is an error.
  • Skipping cancelled events. The option ignoreCancelled: true skips your listener when an earlier one already cancelled the event.
  • Monitor. A monitor listener sees the final event and can't change it: use it to keep records.
// a ward: blows taken in a warded place are halved, unless something already stopped them
wt.on('damage.before', { priority: 'high', ignoreCancelled: true }, async (e) => {
  if (e.dir !== 'taken') return;
  const here = await wt.places.here();
  if (here && here.tags.includes('warded')) e.amount = Math.floor(e.amount / 2);
});

// a monitor only watches: it sees the final amount
wt.on('damage.before', { priority: 'monitor' }, (e) => {
  wt.log('a blow of', e.amount, e.cancelled ? '(stopped)' : '');
});

Every event

What a listener changes is read back after it returns; the next listener sees it. The last column says whether the event fires inside a party fight's rounds (see below); a dash is a moment that isn't part of a fight.

EventWhen it firesWhat it carriesChange or cancelIn party rounds
turn.startA player's turn begins: after facts owed from before and timers come due, before the storyteller reads the scene.action, place–No
turn.endAfter the scene is told.action, moved–No
check.beforeA rule-system roll is about to be made (not a fight's own blows).check, checkName, stat, labels, actionChange modify (a number added to the roll), shift (steps for a label), reason. Cancel: the roll is blocked, and the reason told.Yes
check.resultThe dice are rolled; nothing has landed yet.check, checkName, stat, band, bandName, tone, total, target, margin, bandsChange band, or call e.setBand(id), to move the result. e.reroll() rolls again from the turn's seed (up to three rerolls of one roll); the roll chip shows it.Yes
check.afterThe roll has landed and its effects are applied.check, checkName, stat, band, bandName, tone, total, target, margin, applied–Yes
damage.beforeA blow is about to land, on the player or by them.amount, dir, target, source, attack, critical, textChange amount. Cancel: no damage.Yes
damage.afterA blow landed this scene (read after the scene).amount, dir, target, source, hp, died–No
npc.turnA foe declares its action in a fight.npc, target, actionCancel: the foe loses the action. e.extra(text): an extra action, told to the storyteller in a solo fight.Yes (cancel only)
npc.hurtA blow at an NPC is about to land.amount, npc, source, attack, critical, textChange amount. Cancel: no damage.Yes
npc.downAn NPC fell this scene.npc, by–No
defeatThe player beat a foe this scene.foe–No
deathThe player falls: goes down, or dies for good (permanent is true).cause, killer, place, permanentCancel: the plugin takes the death over. The fall is cleared and what happens next is whatever the listener does; if it heals no one, they stand at 1 health.Yes
restThe storyteller gives them a rest.kind, place––
place.enterThey arrived somewhere, however they moved: the storyteller's move, a voyage, party travel, a plugin, a landfall.place, from–No
place.leaveThey left somewhere, however they moved.place, to–No
npc.metThey met someone for the first time this scene.npc–No
item.gainThe storyteller gave them an item.item, quantity–No
item.loseAn item was taken from them or spent.item, quantity–No
quest.stepA step of a quest was done.quest, step–No
quest.finishA quest ended.quest, outcome–No
fight.startThe storyteller started a fight.enemies–No
fight.endThe storyteller ended a fight.outcome–No
dayA new day on their clock, outside any turn.day, timeOfDay––
levelThey gained a level.from, level––
character.createA new character is saved, before their first scene.start, region, backstoryChange start (a place id): where they begin. The opening scene is written there.–
storyOne of the plugin's own questions was answered yes after a scene.story, question–No
commandA player pressed one of the plugin's buttons (it goes to that button's listener).data, text, place––
scheduledA timer the plugin set came due: checked at the start of a turn and when a day passes.key, data––
rollA roll card the plugin offered with wt.roll was thrown, or threw itself when it ran out.key, data, check, checkName, stat, band, bandName, tone, total, target, margin, applied, thrown––
quest.offerSomeone offered them a quest this scene.quest, from, type––
money.changeThe storyteller changed their money this scene.amount, currency––
dungeon.roomThey moved to another room of a dungeon.room, noise––
voyage.startThey set sail or booked a crossing.action, to, from, days––
voyage.endA crossing ended ashore.to––
item.equipThey put something on or took it in hand.item, displaced––
item.unequipThey took something off.item––
item.craftThey tried to make something at the crafting bench.success, item, rarity, ingredients––
party.joinThey joined a party.partyId, members––
party.leaveThey left a party, or were put out of it.partyId––

Moving between places

place.enter and place.leave fire wherever a character moves: the storyteller moving them in a scene, a voyage, party travel, a plugin's moveTo, a landfall. What a listener tells the storyteller about a move made before a turn (a crossing that lands, a button that moves them) reaches that same turn; about the storyteller's own move in a scene, the next turn.

Your own events

Plugins work together through events of their own. One announces a moment with wt.emit, named in dotted words (bounty.raised, guild.member-joined). Every other plugin that listens for that name hears it, in priority order, with e.character, the fields it carries, and e.from, the id of the plugin that sent it.

// the bounty ledger: a crime raises the bounty, and asks the other plugins what it comes to
const r = await wt.emit('bounty.raised', { amount: 120 });
if (!r.cancelled) c.store.bounty = (c.store.bounty || 0) + r.fields.amount;

// the harbour watch, another plugin: big bounties double, a pardoned pirate goes free
wt.on('bounty.raised', async (e) => {
  if (e.character.store.pardoned) return e.cancel();
  if (e.amount > 100) e.amount = e.amount * 2;
});

Listeners can change its fields or cancel it, just as with the game's own events, and the plugin that sent it reads back what they made of it: whether anyone heard (heard), whether one cancelled it (cancelled), and its fields. So an event can also ask a question: "may this pirate dock here?", answered by a port's plugin cancelling it or setting a price.

An event never comes back to the plugin that sent it, and events sent while another is being heard go at most three deep. Say in your plugin's description which events it sends and what they carry, so other plugins can listen. When you save, a listener on a name that is neither one of the game's events nor a dotted name of your own is refused, and one a letter away from the game's (turn.strat) asks if you meant that one.

In a party fight

A party fight is played in rounds: each hero acts, the blows land, then the round is told as one scene. Inside a round, plugins hear each hero's rolls (check.before, check.result, check.after), every blow before it lands (damage.before, npc.hurt), each foe's declared action (npc.turn: cancelling works, an extra action isn't told) and each hero's fall (death).

The events read from a told scene (turn.start, turn.end, moving between places, meetings, items, quests, defeats, a fight's start and end, story questions) are not fired for a round's shared scene. They fire again on the turns before and after the fight.

When the storyteller hears it

wt.gm.tell gives the storyteller a fact. Told before the scene is written (at turn.start, a roll, a blow, a death), it reaches this turn's storyteller. Told after the scene (turn.end and the other events read from it) or outside a turn (a new day, a timer, a button), it is held and told once, on that character's next turn.

Questions after the scene

wt.story(id, question, listener) asks a yes-or-no question about each scene. After each scene, the questions of every plugin in the world are judged together in one call, up to twelve; a clear yes runs that question's own listener and fires story. This is how a plugin notices what no tool marks: a contract taken, a revel joined, an order carried out.

wt.story('oath', 'Did the character swear an oath in this scene and mean it?', async (e) => {
  e.character.store.oaths = (e.character.store.oaths || 0) + 1;
  await wt.gm.remember('oaths', e.character.name + ' has sworn ' + e.character.store.oaths + ' oaths and is known to keep them.');
});

Events that set off events

What a plugin does can set off moments for other plugins (a storyteller's tool that deals damage fires damage.before). These nest up to three deep; past that, the inner moments aren't fired.